From e3168ea0f54293dc426982b72b5991bbc799c7e6 Mon Sep 17 00:00:00 2001 From: Marsyew Date: Wed, 4 Sep 2024 16:50:33 +0800 Subject: [PATCH 01/15] add:whipinto/whepfrom audio support --- tools/whepfrom/src/main.rs | 57 ++++++++++--- tools/whipinto/src/main.rs | 137 ++++++++++++++++++++++++------- tools/whipinto/src/payload.rs | 2 + tools/whipinto/src/rtspclient.rs | 50 ++++++++++- 4 files changed, 200 insertions(+), 46 deletions(-) diff --git a/tools/whepfrom/src/main.rs b/tools/whepfrom/src/main.rs index e54faa79..f77f69fb 100644 --- a/tools/whepfrom/src/main.rs +++ b/tools/whepfrom/src/main.rs @@ -57,6 +57,8 @@ struct Args { target: String, #[arg(short, long, value_enum)] codec: Codec, + #[arg(short, long, value_enum)] + audio_codec: Codec, /// value: [96, 127] #[arg(short, long, default_value_t = 96)] payload_type: u8, @@ -90,6 +92,7 @@ async fn main() -> Result<()> { let host = args.host.clone(); let mut _codec = args.codec; + let audio_codec = args.audio_codec; let mut rtp_port = args.port; let udp_socket = UdpSocket::bind(format!("{}:0", host)).await?; @@ -107,6 +110,7 @@ async fn main() -> Result<()> { let (peer, answer) = webrtc_start( &mut client, args.codec.into(), + audio_codec.into(), send, payload_type, complete_tx.clone(), @@ -253,20 +257,28 @@ async fn rtcp_listener(host: String, rtcp_port: u16, peer: Arc>, payload_type: u8, complete_tx: UnboundedSender<()>, ) -> Result<(Arc, RTCSessionDescription)> { + let audio_payload_type = payload_type + 1; let peer = new_peer( RTCRtpCodecParameters { capability: codec, payload_type, stats_id: Default::default(), }, + RTCRtpCodecParameters { + capability: audio_codec, + payload_type: audio_payload_type, + stats_id: Default::default(), + }, complete_tx.clone(), send, ) - .await?; + .await + .map_err(|error| anyhow!(format!("[{}] {}", PREFIX_LIB, error)))?; let offer = peer.create_offer(None).await?; let mut gather_complete = peer.gathering_complete_promise().await; @@ -290,38 +302,45 @@ async fn webrtc_start( } async fn new_peer( - codec: RTCRtpCodecParameters, + video_codec: RTCRtpCodecParameters, + audio_codec: RTCRtpCodecParameters, complete_tx: UnboundedSender<()>, sender: UnboundedSender>, ) -> Result> { - let ct = get_codec_type(&codec.capability); + let video_ct = get_codec_type(&video_codec.capability); + let audio_ct = get_codec_type(&audio_codec.capability); let mut m = MediaEngine::default(); - m.register_codec(codec, ct)?; + + m.register_codec(video_codec, video_ct)?; + + m.register_codec(audio_codec, audio_ct)?; + let mut registry = Registry::new(); registry = register_default_interceptors(registry, &mut m)?; let api = APIBuilder::new() .with_media_engine(m) .with_interceptor_registry(registry) .build(); + let config = RTCConfiguration { - ice_servers: vec![{ - RTCIceServer { - urls: vec!["stun:stun.l.google.com:19302".to_string()], - username: "".to_string(), - credential: "".to_string(), - credential_type: RTCIceCredentialType::Unspecified, - } + ice_servers: vec![RTCIceServer { + urls: vec!["stun:stun.l.google.com:19302".to_string()], + username: "".to_string(), + credential: "".to_string(), + credential_type: RTCIceCredentialType::Unspecified, }], ..Default::default() }; + let peer = Arc::new( api.new_peer_connection(config) .await .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?, ); + let _ = peer .add_transceiver_from_kind( - ct, + video_ct, Some(RTCRtpTransceiverInit { direction: RTCRtpTransceiverDirection::Recvonly, send_encodings: vec![], @@ -329,6 +348,18 @@ async fn new_peer( ) .await .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; + + let _ = peer + .add_transceiver_from_kind( + audio_ct, + Some(RTCRtpTransceiverInit { + direction: RTCRtpTransceiverDirection::Recvonly, + send_encodings: vec![], + }), + ) + .await + .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; + let pc = peer.clone(); peer.on_peer_connection_state_change(Box::new(move |s| { let pc = pc.clone(); @@ -347,6 +378,7 @@ async fn new_peer( }); Box::pin(async {}) })); + peer.on_track(Box::new(move |track, _, _| { let sender = sender.clone(); tokio::spawn(async move { @@ -360,5 +392,6 @@ async fn new_peer( }); Box::pin(async {}) })); + Ok(peer) } diff --git a/tools/whipinto/src/main.rs b/tools/whipinto/src/main.rs index ea565d86..34f50bff 100644 --- a/tools/whipinto/src/main.rs +++ b/tools/whipinto/src/main.rs @@ -104,8 +104,10 @@ async fn main() -> Result<()> { host = h; } - let mut codec = Codec::Vp8; + let mut video_codec = Codec::Vp8; + let mut audio_codec = Codec::Opus; let mut rtp_port = input.port().unwrap_or(0); + let mut audio_port = 0; let mut rtcp_send_port = 0; let (complete_tx, mut complete_rx) = unbounded_channel(); @@ -171,12 +173,29 @@ async fn main() -> Result<()> { rtp_port = rtp_server_port; rtcp_send_port = rtcp_listen_port; } else if input.scheme() == SCHEME_RTSP_CLIENT { - (rtp_port, codec) = setup_rtsp_session(&args.input).await?; + (rtp_port, audio_port, video_codec, audio_codec) = setup_rtsp_session(&args.input).await?; } else { let sdp = sdp_types::Session::parse(&fs::read(args.input).unwrap()).unwrap(); let video_track = sdp.medias.iter().find(|md| md.media == "video"); + let audio_track = sdp.medias.iter().find(|md| md.media == "audio"); - let video_codec = video_track + let codec_vid = video_track + .and_then(|md| { + md.attributes.iter().find_map(|attr| { + if attr.attribute == "rtpmap" { + let parts: Vec<&str> = attr.value.as_ref()?.split_whitespace().collect(); + if parts.len() > 1 { + Some(parts[1].split('/').next().unwrap_or("").to_string()) + } else { + None + } + } else { + None + } + }) + }) + .unwrap_or_else(|| "unknown".to_string()); + let codec_aud = audio_track .and_then(|md| { md.attributes.iter().find_map(|attr| { if attr.attribute == "rtpmap" { @@ -193,8 +212,10 @@ async fn main() -> Result<()> { }) .unwrap_or_else(|| "unknown".to_string()); - codec = rtspclient::codec_from_str(&video_codec)?; + video_codec = rtspclient::codec_from_str(&codec_vid)?; + audio_codec = rtspclient::codec_from_str(&codec_aud)?; rtp_port = video_track.unwrap().port; + audio_port = audio_track.unwrap().port; } debug!("use rtp port {}", rtp_port); @@ -205,6 +226,13 @@ async fn main() -> Result<()> { listener.local_addr().unwrap() ); + debug!("use audio_rtp port {}", audio_port); + let audio_listener = UdpSocket::bind(format!("{}:{}", host, audio_port)).await?; + let _audio_port = listener.local_addr()?.port(); + info!( + "=== audio_RTP listener started : {} ===", + audio_listener.local_addr().unwrap() + ); let mut client = Client::new( args.whip, Client::get_auth_header_map(args.auth_basic, args.auth_token), @@ -222,11 +250,17 @@ async fn main() -> Result<()> { } } }); - let (peer, sender) = webrtc_start(&mut client, codec.into(), complete_tx.clone()) - .await - .map_err(|error| anyhow!(format!("[{}] {}", PREFIX_LIB, error)))?; - - tokio::spawn(rtp_listener(listener, sender)); + let (peer, video_sender, audio_sender) = webrtc_start( + &mut client, + video_codec.into(), + audio_codec.into(), + complete_tx.clone(), + ) + .await + .map_err(|error| anyhow!(format!("[{}] {}", PREFIX_LIB, error)))?; + + tokio::spawn(rtp_listener(listener, video_sender)); + tokio::spawn(rtp_listener(audio_listener, audio_sender)); if input.scheme() == SCHEME_RTSP_SERVER { let rtcp_port = rtp_port + 1; tokio::spawn(rtcp_listener(host.clone(), rtcp_port, peer.clone())); @@ -330,10 +364,16 @@ async fn read_rtcp(sender: Arc, host: String, port: u16) -> Result async fn webrtc_start( client: &mut Client, - codec: RTCRtpCodecCapability, + video_codec: RTCRtpCodecCapability, + audio_codec: RTCRtpCodecCapability, complete_tx: UnboundedSender<()>, -) -> Result<(Arc, UnboundedSender>)> { - let (peer, sender) = new_peer(codec, complete_tx.clone()).await?; +) -> Result<( + Arc, + UnboundedSender>, + UnboundedSender>, +)> { + let (peer, video_sender, audio_sender) = + new_peer(video_codec, audio_codec, complete_tx.clone()).await?; let offer = peer.create_offer(None).await?; let mut gather_complete = peer.gathering_complete_promise().await; @@ -353,13 +393,18 @@ async fn webrtc_start( .await .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; - Ok((peer, sender)) + Ok((peer, video_sender, audio_sender)) } async fn new_peer( - codec: RTCRtpCodecCapability, + video_codec: RTCRtpCodecCapability, + audio_codec: RTCRtpCodecCapability, complete_tx: UnboundedSender<()>, -) -> Result<(Arc, UnboundedSender>)> { +) -> Result<( + Arc, + UnboundedSender>, + UnboundedSender>, +)> { let mut m = MediaEngine::default(); m.register_default_codecs()?; let mut registry = Registry::new(); @@ -404,36 +449,68 @@ async fn new_peer( }); Box::pin(async {}) })); - let mime_type = codec.mime_type.clone(); - let track = Arc::new(TrackLocalStaticRTP::new( - codec, - "webrtc".to_owned(), - "webrtc-rs".to_owned(), + let video_mime_type = video_codec.mime_type.clone(); + let video_track = Arc::new(TrackLocalStaticRTP::new( + video_codec, + "webrtc-video".to_owned(), + "webrtc-video-rs".to_owned(), )); let _ = peer - .add_track(track.clone() as Arc) + .add_track(video_track.clone() as Arc) .await .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; - let (send, mut recv) = unbounded_channel::>(); + let (video_tx, mut video_rx) = unbounded_channel::>(); + + let audio_mime_type = audio_codec.mime_type.clone(); + let audio_track = Arc::new(TrackLocalStaticRTP::new( + audio_codec, + "webrtc-audio".to_owned(), + "webrtc-audio-rs".to_owned(), + )); + let _ = peer + .add_track(audio_track.clone() as Arc) + .await + .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; + let (audio_tx, mut audio_rx) = unbounded_channel::>(); tokio::spawn(async move { - debug!("Codec is: {}", mime_type); - let mut handler: Box = match mime_type.as_str() { - MIME_TYPE_VP8 => Box::new(payload::RePayloadCodec::new(mime_type)), - MIME_TYPE_VP9 => Box::new(payload::RePayloadCodec::new(mime_type)), - MIME_TYPE_H264 => Box::new(payload::RePayloadCodec::new(mime_type)), + debug!("Codec is: {}", video_mime_type); + let mut handler: Box = match video_mime_type.as_str() { + MIME_TYPE_VP8 => Box::new(payload::RePayloadCodec::new(video_mime_type)), + MIME_TYPE_VP9 => Box::new(payload::RePayloadCodec::new(video_mime_type)), + MIME_TYPE_H264 => Box::new(payload::RePayloadCodec::new(video_mime_type)), _ => Box::new(payload::Forward::new()), }; - while let Some(data) = recv.recv().await { + while let Some(data) = video_rx.recv().await { if let Ok(packet) = Packet::unmarshal(&mut data.as_slice()) { trace!("received packet: {}", packet); for packet in handler.payload(packet) { trace!("send packet: {}", packet); - let _ = track.write_rtp(&packet).await; + let _ = video_track.write_rtp(&packet).await; + } + } + } + }); + + tokio::spawn(async move { + debug!("Codec is: {}", audio_mime_type); + let mut handler: Box = match audio_mime_type.as_str() { + MIME_TYPE_OPUS => Box::new(payload::RePayloadCodec::new(audio_mime_type.clone())), + _ => Box::new(payload::Forward::new()), + }; + + while let Some(data) = audio_rx.recv().await { + if audio_mime_type == MIME_TYPE_G722 { + let _ = audio_track.write(&data).await; + } else if let Ok(packet) = Packet::unmarshal(&mut data.as_slice()) { + trace!("received packet: {}", packet); + for packet in handler.payload(packet) { + trace!("send packet: {}", packet); + let _ = audio_track.write_rtp(&packet).await; } } } }); - Ok((peer, send)) + Ok((peer, video_tx, audio_tx)) } diff --git a/tools/whipinto/src/payload.rs b/tools/whipinto/src/payload.rs index c079110b..09473112 100644 --- a/tools/whipinto/src/payload.rs +++ b/tools/whipinto/src/payload.rs @@ -79,12 +79,14 @@ impl RePayloadCodec { MIME_TYPE_VP8 => Box::default() as Box, MIME_TYPE_VP9 => Box::default() as Box, MIME_TYPE_H264 => Box::default() as Box, + MIME_TYPE_OPUS => Box::default() as Box, _ => Box::default() as Box, }, encoder: match mime_type.as_str() { MIME_TYPE_VP8 => Box::default() as Box, MIME_TYPE_VP9 => Box::default() as Box, MIME_TYPE_H264 => Box::default() as Box, + MIME_TYPE_OPUS => Box::default() as Box, _ => Box::default() as Box, }, } diff --git a/tools/whipinto/src/rtspclient.rs b/tools/whipinto/src/rtspclient.rs index 984d41e3..a9717558 100644 --- a/tools/whipinto/src/rtspclient.rs +++ b/tools/whipinto/src/rtspclient.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Ok, Result}; use cli::Codec; use md5::{Digest, Md5}; use portpicker::pick_unused_port; @@ -41,7 +41,8 @@ pub fn codec_from_str(s: &str) -> Result { "AV1" => Ok(Codec::AV1), "OPUS" => Ok(Codec::Opus), "G722" => Ok(Codec::G722), - _ => Err(anyhow!("Unknown codec: {}", s)), + // _ => Err(anyhow!("Unknown codec: {}", s)), + _ => Ok(Codec::G722), } } @@ -295,7 +296,7 @@ impl RtspSession { } } -pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, Codec)> { +pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Codec)> { let mut url = Url::parse(rtsp_url)?; let host = url .host() @@ -339,6 +340,7 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, Codec)> { } let rtp_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; + let rtp_audio_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; let video_uri = video_track .and_then(|md| { @@ -357,7 +359,25 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, Codec)> { }) .unwrap_or_else(|| format!("{}/trackID=1", rtsp_session.uri)); + let audio_uri = audio_track + .and_then(|md| { + md.attributes.iter().find_map(|attr| { + if attr.attribute == "control" { + let value = attr.value.clone().unwrap_or_default(); + if value.starts_with("rtsp://") { + Some(value) + } else { + Some(format!("{}/{}", rtsp_session.uri, value)) + } + } else { + None + } + }) + }) + .unwrap_or_else(|| format!("{}/trackID=2", rtsp_session.uri)); + trace!("video uri: {:?}", video_uri); + trace!("audio uri: {:?}", audio_uri); rtsp_session.uri.clone_from(&video_uri); rtsp_session.rtp_client_port = Some(rtp_port); @@ -367,6 +387,10 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, Codec)> { rtsp_session.session_id = Some(session_id); + rtsp_session.uri.clone_from(&audio_uri); + rtsp_session.rtp_client_port = Some(rtp_audio_port); + rtsp_session.send_setup_request().await?; + let play_request = Request::builder(Method::Play, Version::V1_0) .request_uri( rtsp_session @@ -419,7 +443,25 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, Codec)> { }) .unwrap_or_else(|| "unknown".to_string()); + let audio_codec = audio_track + .and_then(|md| { + md.attributes.iter().find_map(|attr| { + if attr.attribute == "rtpmap" { + let parts: Vec<&str> = attr.value.as_ref()?.split_whitespace().collect(); + if parts.len() > 1 { + Some(parts[1].split('/').next().unwrap_or("").to_string()) + } else { + None + } + } else { + None + } + }) + }) + .unwrap_or_else(|| "unknown".to_string()); + let video_codec = codec_from_str(&video_codec)?; + let audio_codec = codec_from_str(&audio_codec)?; - Ok((rtp_port, video_codec)) + Ok((rtp_port, rtp_audio_port, video_codec, audio_codec)) } From d85dc95cdc44174ee69c8cb5f95553238ce83b62 Mon Sep 17 00:00:00 2001 From: Marsyew Date: Sun, 29 Sep 2024 15:21:30 +0800 Subject: [PATCH 02/15] add whipinto/whepfrom audio support --- Cargo.lock | 5 + libs/cli/src/lib.rs | 16 +- libs/rtsp/Cargo.toml | 4 +- libs/rtsp/src/lib.rs | 346 ++++++++++++++++++----- tools/whepfrom/Cargo.toml | 4 +- tools/whepfrom/src/main.rs | 436 +++++++++++++++++------------ tools/whipinto/src/main.rs | 464 +++++++++++++++++-------------- tools/whipinto/src/rtspclient.rs | 44 ++- tools/whipinto/test.sdp | 13 + 9 files changed, 860 insertions(+), 472 deletions(-) create mode 100644 tools/whipinto/test.sdp diff --git a/Cargo.lock b/Cargo.lock index 602b0b04..2482b1fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,11 +1999,13 @@ name = "rtsp" version = "0.1.0" dependencies = [ "anyhow", + "cli", "portpicker", "rtsp-types", "sdp 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "sdp-types", "tokio", + "webrtc", ] [[package]] @@ -3212,11 +3214,14 @@ dependencies = [ "clap", "cli", "libwish", + "portpicker", "rtsp", "scopeguard", + "sdp 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "signal", "tokio", "tracing", + "url", "utils", "webrtc", ] diff --git a/libs/cli/src/lib.rs b/libs/cli/src/lib.rs index da81d86c..60887296 100644 --- a/libs/cli/src/lib.rs +++ b/libs/cli/src/lib.rs @@ -3,7 +3,7 @@ use std::{ sync::Mutex, }; -use anyhow::Result; +use anyhow::{anyhow, Result}; use clap::ValueEnum; use webrtc::{ api::media_engine::*, @@ -13,7 +13,7 @@ use webrtc::{ }, }; -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +#[derive(Copy, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] pub enum Codec { Vp8, Vp9, @@ -108,6 +108,18 @@ impl From for RTCRtpCodecCapability { } } +pub fn codec_from_str(s: &str) -> Result { + match s.to_uppercase().as_str() { + "VP8" => Ok(Codec::Vp8), + "VP9" => Ok(Codec::Vp9), + "H264" => Ok(Codec::H264), + "AV1" => Ok(Codec::AV1), + "OPUS" => Ok(Codec::Opus), + "G722" => Ok(Codec::G722), + _ => Err(anyhow!("Unknown codec: {}", s)), + } +} + pub fn get_codec_type(codec: &RTCRtpCodecCapability) -> RTPCodecType { let mime_type = &codec.mime_type; if mime_type.starts_with("video") { diff --git a/libs/rtsp/Cargo.toml b/libs/rtsp/Cargo.toml index 13b017c8..7a52636e 100644 --- a/libs/rtsp/Cargo.toml +++ b/libs/rtsp/Cargo.toml @@ -13,4 +13,6 @@ rtsp-types = "0.1.1" sdp = "0.6" tokio = "1.37" sdp-types = "0.1.6" -portpicker = "0.1.1" \ No newline at end of file +portpicker = "0.1.1" +cli = { path = "../cli" } +webrtc = { workspace = true } \ No newline at end of file diff --git a/libs/rtsp/src/lib.rs b/libs/rtsp/src/lib.rs index 2c9adb69..4f2f7e99 100644 --- a/libs/rtsp/src/lib.rs +++ b/libs/rtsp/src/lib.rs @@ -1,31 +1,75 @@ use anyhow::{anyhow, Error, Result}; +use cli::{codec_from_str, Codec}; use portpicker::pick_unused_port; use rtsp_types::ParseError; use rtsp_types::{headers, headers::transport, Message, Method, Request, Response, StatusCode}; +use sdp::{ + description::common::Attribute, + SessionDescription, +}; use sdp_types::Session; +use std::io::Cursor; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream, sync::mpsc::UnboundedSender, }; +use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecParameters; const SERVER_NAME: &str = "whipinto"; #[derive(Debug, Clone)] pub struct Handler { sdp: Option>, - rtp: Option, - rtcp: Option, - up_tx: UnboundedSender, + media_info: MediaInfo, + up_tx: UnboundedSender, dn_tx: UnboundedSender<()>, } +#[derive(Debug, Clone)] +pub struct MediaInfo { + pub video_rtp_client: Option, + pub video_rtcp_client: Option, + pub video_rtp_server: Option, + pub audio_rtp_client: Option, + pub audio_rtp_server: Option, + pub video_codec: Option, + pub audio_codec: Option, +} + +#[derive(Clone, Debug)] +pub struct CodecInfo { + pub video_codec: Option, + pub audio_codec: Option, +} + +impl CodecInfo { + pub fn new() -> Self { + Self { + video_codec: None, + audio_codec: None, + } + } +} + +impl Default for MediaInfo { + fn default() -> Self { + Self { + video_rtp_client: None, + video_rtcp_client: None, + video_rtp_server: None, + audio_rtp_client: None, + audio_rtp_server: None, + video_codec: None, + audio_codec: None, + } + } +} impl Handler { - pub fn new(up_tx: UnboundedSender, dn_tx: UnboundedSender<()>) -> Handler { + pub fn new(up_tx: UnboundedSender, dn_tx: UnboundedSender<()>) -> Handler { Self { sdp: None, - rtp: None, - rtcp: None, + media_info: MediaInfo::default(), up_tx, dn_tx, } @@ -35,19 +79,23 @@ impl Handler { self.sdp = Some(sdp); } - pub fn get_rtp(&self) -> u16 { - self.rtp.unwrap() - } - - pub fn get_rtcp(&self) -> u16 { - self.rtcp.unwrap() - } - fn todo(&self, req: &Request>) -> Response> { unimplemented!("{:?}", req.method()); } fn play(&self, req: &Request>) -> Response> { + self.up_tx + .send(MediaInfo { + video_rtp_client: self.media_info.video_rtp_client, + video_rtcp_client: self.media_info.video_rtcp_client, + video_rtp_server: self.media_info.video_rtp_server, + audio_rtp_client: self.media_info.audio_rtp_client, + audio_rtp_server: self.media_info.audio_rtp_server, + video_codec: self.media_info.video_codec.clone(), + audio_codec: self.media_info.audio_codec.clone(), + }) + .unwrap(); + Response::builder(req.version(), StatusCode::Ok) .header(headers::CSEQ, req.header(&headers::CSEQ).unwrap().as_str()) .header(headers::SERVER, "whipinto") @@ -55,13 +103,27 @@ impl Handler { } fn record(&self, req: &Request>) -> Response> { + self.up_tx + .send(MediaInfo { + video_rtp_client: self.media_info.video_rtp_client, + video_rtcp_client: self.media_info.video_rtcp_client, + video_rtp_server: self.media_info.video_rtp_server, + audio_rtp_client: self.media_info.audio_rtp_client, + audio_rtp_server: self.media_info.audio_rtp_server, + video_codec: self.media_info.video_codec.clone(), + audio_codec: self.media_info.audio_codec.clone(), + }) + .unwrap(); + + println!("recordrrrrr {:?}", self.media_info); + Response::builder(req.version(), StatusCode::Ok) .header(headers::CSEQ, req.header(&headers::CSEQ).unwrap().as_str()) .header(headers::SERVER, "whipinto") .build(self.sdp.clone().unwrap()) } - fn describe(&self, req: &Request>) -> Response> { + fn describe(&mut self, req: &Request>) -> Response> { if self.sdp.is_none() { println!("sdp is none"); } @@ -81,38 +143,132 @@ impl Handler { match tr { transport::Transport::Rtp(rtp_transport) => { - println!("rtp_transport {:?}", rtp_transport); let (rtp, rtcp) = rtp_transport.params.client_port.unwrap(); - println!("rtp: {:?}, rtcp: {:?}", rtp, rtcp); - self.rtp = Some(rtp); - self.rtcp = rtcp; - self.up_tx.send(rtp.to_string()).unwrap(); - self.up_tx.send(rtcp.unwrap().to_string()).unwrap(); - } - transport::Transport::Other(other_transport) => { - println!("other_transport {:?}", other_transport); + let uri = req.request_uri().unwrap().as_str(); + + let url_id = uri + .split("streamid=") + .nth(1) + .and_then(|id_str| id_str.split('&').next()) + .map(|id| id.to_string()); + + if let Some(sdp_data) = &self.sdp { + let sdp = sdp_types::Session::parse(sdp_data).unwrap(); + + for media in sdp.medias.iter() { + let media_control = media + .attributes + .iter() + .find(|attr| attr.attribute == "control") + .and_then(|attr| attr.value.as_deref()) + .and_then(|control| { + if control.contains("streamid=") { + control.split("streamid=").nth(1).map(|id| id.to_string()) + } else { + None + } + }); + + if media.media == "audio" && media_control.as_deref() == url_id.as_deref() { + let audio_server_port = + pick_unused_port().expect("Failed to find an unused audio port"); + let audio_rtcp_server_port = audio_server_port + 1; + + self.media_info.audio_rtp_client = Some(rtp); + self.media_info.audio_rtp_server = Some(audio_server_port); + self.media_info.audio_codec = media + .attributes + .iter() + .find(|attr| attr.attribute == "rtpmap") + .and_then(|attr| attr.value.as_ref()) + .and_then(|value| { + value + .split_whitespace() + .nth(1) + .unwrap_or("") + .split('/') + .next() + .map(|codec_str| codec_from_str(codec_str).ok()) + }) + .unwrap_or(None); + + return Response::builder(req.version(), StatusCode::Ok) + .header(headers::CSEQ, req.header(&headers::CSEQ).unwrap().as_str()) + .header(headers::SERVER, SERVER_NAME) + .header(headers::SESSION, "1111-2222-3333-4444") + .typed_header(&transport::Transports::from(vec![ + transport::Transport::Rtp(transport::RtpTransport { + profile: transport::RtpProfile::Avp, + lower_transport: None, + params: transport::RtpTransportParameters { + unicast: true, + server_port: Some(( + audio_server_port, + Some(audio_rtcp_server_port), + )), + ..Default::default() + }, + }), + ])) + .build(Vec::new()); + } else if media.media == "video" + && media_control.as_deref() == url_id.as_deref() + { + let video_server_port = + pick_unused_port().expect("Failed to find an unused video port"); + let video_rtcp_server_port = video_server_port + 1; + + self.media_info.video_rtp_client = Some(rtp); + self.media_info.video_rtcp_client = rtcp; + self.media_info.video_rtp_server = Some(video_server_port); + self.media_info.video_codec = media + .attributes + .iter() + .find(|attr| attr.attribute == "rtpmap") + .and_then(|attr| attr.value.as_ref()) + .and_then(|value| { + value + .split_whitespace() + .nth(1) + .unwrap_or("") + .split('/') + .next() + .map(|codec_str| codec_from_str(codec_str).ok()) + }) + .unwrap_or(None); + + return Response::builder(req.version(), StatusCode::Ok) + .header(headers::CSEQ, req.header(&headers::CSEQ).unwrap().as_str()) + .header(headers::SERVER, SERVER_NAME) + .header(headers::SESSION, "1111-2222-3333-4444") + .typed_header(&transport::Transports::from(vec![ + transport::Transport::Rtp(transport::RtpTransport { + profile: transport::RtpProfile::Avp, + lower_transport: None, + params: transport::RtpTransportParameters { + unicast: true, + server_port: Some(( + video_server_port, + Some(video_rtcp_server_port), + )), + ..Default::default() + }, + }), + ])) + .build(Vec::new()); + } + } + println!("Updated self.media_info: {:?}", self.media_info); + } else { + println!("SDP data is not available"); + } } - }; - let rtp_server_port = pick_unused_port().expect("Failed to find an unused port"); - let rtcp_server_port = rtp_server_port + 1; - self.up_tx.send(rtp_server_port.to_string()).unwrap(); + _ => {} + } Response::builder(req.version(), StatusCode::Ok) .header(headers::CSEQ, req.header(&headers::CSEQ).unwrap().as_str()) .header(headers::SERVER, SERVER_NAME) - .header(headers::SESSION, "1111-2222-3333-4444") - .typed_header(&transport::Transports::from(vec![ - transport::Transport::Rtp(transport::RtpTransport { - profile: transport::RtpProfile::Avp, - lower_transport: None, - params: transport::RtpTransportParameters { - unicast: true, - //client_port: Some((18704, Some(18705))), - server_port: Some((rtp_server_port, Some(rtcp_server_port))), - ..Default::default() - }, - }), - ])) .build(Vec::new()) } @@ -120,28 +276,6 @@ impl Handler { self.set_sdp(req.body().to_vec()); let sdp = Session::parse(req.body()).unwrap(); println!("parsed sdp: {:?}", sdp); - // self.set_sdp(req.body().to_vec()); - // sdp-types = "0.1.6" - // https://crates.io/crates/sdp-types - // let sdp = sdp_types::Session::parse(req.body()).unwrap(); - // let rtpmap = sdp.medias.first().unwrap().attributes.first().unwrap().value.clone().unwrap_or("".to_string()); - - // webrtc-sdp - // let sdp = sdp::description::session::SessionDescription::unmarshal( - // &mut std::io::Cursor::new(req.body()), - // ) - // .unwrap(); - // println!("{:?}", sdp); - //let rtpmap = sdp - // .media_descriptions - // .first() - // .unwrap() - // .attributes - // .first() - // .unwrap() - // .value - // .clone() - // .unwrap_or("".to_string()); Response::builder(req.version(), StatusCode::Ok) .header(headers::CSEQ, req.header(&headers::CSEQ).unwrap().as_str()) @@ -227,3 +361,89 @@ pub async fn process_socket(mut socket: TcpStream, handler: &mut Handler) -> Res } } } + +pub fn filter_sdp( + webrtc_sdp: &str, + video_codec: Option<&RTCRtpCodecParameters>, + audio_codec: Option<&RTCRtpCodecParameters>, +) -> Result { + let mut reader = Cursor::new(webrtc_sdp.as_bytes()); + let mut session = match SessionDescription::unmarshal(&mut reader) { + Ok(sdp) => sdp, + Err(e) => return Err(format!("Failed to parse SDP: {:?}", e)), + }; + + session.media_descriptions.retain_mut(|media| { + if media.media_name.media == "video" { + if video_codec.is_none() { + return false; + } else if let Some(video_codec) = video_codec { + media + .media_name + .formats + .retain(|fmt| fmt == &video_codec.payload_type.to_string()); + media.attributes.retain(|attr| { + attr.key == "rtpmap" + && attr.value.as_ref().map_or(false, |v| { + v.starts_with(&video_codec.payload_type.to_string()) + }) + }); + media.media_name.protos = vec!["RTP".to_string(), "AVP".to_string()]; + media.attributes.push(Attribute { + key: "control".to_string(), + value: Some("streamid=0".to_string()), + }); + } + } else if media.media_name.media == "audio" { + if audio_codec.is_none() { + return false; + } else if let Some(audio_codec) = audio_codec { + media + .media_name + .formats + .retain(|fmt| fmt == &audio_codec.payload_type.to_string()); + media.attributes.retain(|attr| { + attr.key == "rtpmap" + && attr.value.as_ref().map_or(false, |v| { + v.starts_with(&audio_codec.payload_type.to_string()) + }) + }); + media.media_name.protos = vec!["RTP".to_string(), "AVP".to_string()]; + media.attributes.push(Attribute { + key: "control".to_string(), + value: Some("streamid=1".to_string()), + }); + } + } + + true + }); + + session.attributes.retain(|attr| { + !attr.key.starts_with("group") + && !attr.key.starts_with("fingerprint") + && !attr.key.starts_with("end-of-candidates") + && !attr.key.starts_with("setup") + && !attr.key.starts_with("mid") + && !attr.key.starts_with("ice-ufrag") + && !attr.key.starts_with("ice-pwd") + && !attr.key.starts_with("extmap") + }); + + for media in &mut session.media_descriptions { + media.attributes.retain(|attr| { + !attr.key.starts_with("rtcp") + && !attr.key.starts_with("ssrc") + && !attr.key.starts_with("candidate") + && !attr.key.starts_with("fmtp") + && !attr.key.starts_with("setup") + && !attr.key.starts_with("mid") + && !attr.key.starts_with("ice-ufrag") + && !attr.key.starts_with("ice-pwd") + && !attr.key.starts_with("extmap") + && !attr.key.starts_with("end-of-candidates") + }); + } + + Ok(session.marshal()) +} diff --git a/tools/whepfrom/Cargo.toml b/tools/whepfrom/Cargo.toml index 210e0aec..271a0f82 100644 --- a/tools/whepfrom/Cargo.toml +++ b/tools/whepfrom/Cargo.toml @@ -18,5 +18,7 @@ clap = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } webrtc = { workspace = true } - scopeguard = "1.2.0" +sdp = "0.6.2" +portpicker = "0.1.1" +url = "2.5.2" diff --git a/tools/whepfrom/src/main.rs b/tools/whepfrom/src/main.rs index f77f69fb..dada376d 100644 --- a/tools/whepfrom/src/main.rs +++ b/tools/whepfrom/src/main.rs @@ -1,15 +1,23 @@ -use std::{sync::Arc, time::Duration}; - use anyhow::{anyhow, Result}; -use clap::{ArgAction, Parser, ValueEnum}; -use core::net::Ipv4Addr; +use clap::{ArgAction, Parser}; +use cli::create_child; +use core::net::{Ipv4Addr, Ipv6Addr}; +use portpicker::pick_unused_port; use scopeguard::defer; +use sdp::{description::media::RangedPort, SessionDescription}; +use std::{ + fs::File, + io::{Cursor, Write}, + sync::Arc, + time::Duration, +}; use tokio::net::TcpListener; use tokio::{ net::UdpSocket, - sync::mpsc::{unbounded_channel, UnboundedSender}, + sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, }; use tracing::{debug, error, info, trace, warn, Level}; +use url::{Host, Url}; use webrtc::ice_transport::ice_credential_type::RTCIceCredentialType; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; use webrtc::{ @@ -22,56 +30,36 @@ use webrtc::{ }, rtcp, rtp_transceiver::{ - rtp_codec::{RTCRtpCodecCapability, RTCRtpCodecParameters}, - rtp_transceiver_direction::RTCRtpTransceiverDirection, + rtp_codec::RTPCodecType, rtp_transceiver_direction::RTCRtpTransceiverDirection, RTCRtpTransceiverInit, }, util::MarshalSize, }; -use cli::{create_child, get_codec_type, Codec}; use libwish::Client; const PREFIX_LIB: &str = "WEBRTC"; - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -enum Mode { - Rtsp, - Rtp, -} +const SCHEME_RTSP_SERVER: &str = "rtsp-listen"; +const _SCHEME_RTSP_CLIENT: &str = "rtsp"; +const SCHEME_RTP_SDP: &str = "sdp"; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { - /// Verbose mode [default: "warn", -v "info", -vv "debug", -vvv "trace"] #[arg(short = 'v', action = ArgAction::Count, default_value_t = 0)] verbose: u8, - #[arg(short, long, value_enum, default_value_t = Mode::Rtsp)] - mode: Mode, - /// Set Listener address - #[arg(long, default_value_t = Ipv4Addr::UNSPECIFIED.to_string())] - host: String, - #[arg(long, default_value_t = 0)] - port: u16, - #[arg(short, long)] - target: String, - #[arg(short, long, value_enum)] - codec: Codec, - #[arg(short, long, value_enum)] - audio_codec: Codec, - /// value: [96, 127] - #[arg(short, long, default_value_t = 96)] - payload_type: u8, - /// The WHEP server endpoint to POST SDP offer to. e.g.: https://example.com/whep/777 + #[arg(short, long, default_value_t = format!("{}://0.0.0.0:8555", SCHEME_RTSP_SERVER))] + output: String, + #[arg(long)] + host: Option, + /// The WHIP server endpoint to POST SDP offer to. e.g.: https://example.com/whip/777 #[arg(short, long)] - url: String, + whep: String, /// Run a command as childprocess #[arg(long)] command: Option, - /// Authentication basic to use, will be sent in the HTTP Header as 'Basic ' e.g.: admin:public #[arg(long)] auth_basic: Option, - /// Authentication token to use, will be sent in the HTTP Header as 'Bearer ' #[arg(long)] auth_token: Option, } @@ -90,44 +78,71 @@ async fn main() -> Result<()> { } )); - let host = args.host.clone(); - let mut _codec = args.codec; - let audio_codec = args.audio_codec; - let mut rtp_port = args.port; + let input = Url::parse(&args.output).unwrap_or( + Url::parse(&format!( + "{}://{}:0/{}", + SCHEME_RTP_SDP, + Ipv4Addr::UNSPECIFIED, + args.output + )) + .unwrap(), + ); + info!("=== Received Input: {} ===", args.output); - let udp_socket = UdpSocket::bind(format!("{}:0", host)).await?; + let mut host = match input.host().unwrap() { + Host::Domain(_) | Host::Ipv4(_) => Ipv4Addr::UNSPECIFIED.to_string(), + Host::Ipv6(_) => Ipv6Addr::UNSPECIFIED.to_string(), + }; + + if let Some(ref h) = args.host { + debug!("=== Specified set host, using {} ===", h); + host = h.clone(); + } let (complete_tx, mut complete_rx) = unbounded_channel(); + let mut media_info = rtsp::MediaInfo::default(); + let (video_send, video_recv) = unbounded_channel::>(); + let (audio_send, audio_recv) = unbounded_channel::>(); + let codec_info = Arc::new(tokio::sync::Mutex::new(rtsp::CodecInfo::new())); - let payload_type = args.payload_type; - assert!((96..=127).contains(&payload_type)); let mut client = Client::new( - args.url, - Client::get_auth_header_map(args.auth_basic, args.auth_token), + args.whep.clone(), + Client::get_auth_header_map(args.auth_basic.clone(), args.auth_token.clone()), ); - let (send, mut recv) = unbounded_channel::>(); let (peer, answer) = webrtc_start( &mut client, - args.codec.into(), - audio_codec.into(), - send, - payload_type, + video_send, + audio_send, complete_tx.clone(), + codec_info.clone(), ) - .await - .map_err(|error| anyhow!(format!("[{}] {}", PREFIX_LIB, error)))?; - - let mut rtcp_port = 0; - if args.mode == Mode::Rtsp { - let (tx, mut rx) = unbounded_channel::(); + .await?; + info!("answer sdp: {:?}", answer.sdp); + + tokio::time::sleep(Duration::from_secs(1)).await; + let codec_info = codec_info.lock().await; + debug!("code info {:?}", codec_info); + + let filtered_sdp = match rtsp::filter_sdp( + &answer.sdp, + codec_info.video_codec.as_ref(), + codec_info.audio_codec.as_ref(), + ) { + Ok(sdp) => sdp, + Err(e) => e, + }; + info!("Filtered SDP: {:?}", filtered_sdp); + if input.scheme() == SCHEME_RTSP_SERVER { + let (tx, mut rx) = unbounded_channel::(); let mut handler = rtsp::Handler::new(tx, complete_tx.clone()); - handler.set_sdp(answer.sdp.as_bytes().to_vec()); - info!("sdp:{:?}", answer.sdp); + handler.set_sdp(filtered_sdp.into_bytes()); + let host2 = host.to_string(); + let tcp_port = input.port().unwrap_or(0); tokio::spawn(async move { - let listener = TcpListener::bind(format!("{}:{}", host, args.port)) + let listener = TcpListener::bind(format!("{}:{}", host2.clone(), tcp_port)) .await .unwrap(); warn!( @@ -144,27 +159,36 @@ async fn main() -> Result<()> { } }); - let (rtp_listen_port, _rtcp_listen_port, rtp_server_port) = - match (rx.recv().await, rx.recv().await, rx.recv().await) { - (Some(rtp), Some(rtcp), Some(rtp_server)) => { - let rtp_port = rtp.parse::().unwrap_or(8002); - let rtcp_port = rtcp.parse::().unwrap_or(8003); - let rtp_server_port = rtp_server.parse::().unwrap_or(8004); - println! {"receive port: {},{},{}",rtp_port,rtcp_port,rtp_server_port}; - (rtp_port, rtcp_port, rtp_server_port) + media_info = rx.recv().await.unwrap(); + + println!("Media info: {:?}", media_info); + } else { + media_info.video_rtp_client = pick_unused_port(); + media_info.audio_rtp_client = pick_unused_port(); + + let mut reader = Cursor::new(filtered_sdp.as_bytes()); + let mut session = SessionDescription::unmarshal(&mut reader).unwrap(); + for media in &mut session.media_descriptions { + if media.media_name.media == "video" { + if let Some(port) = media_info.video_rtp_client { + media.media_name.port = RangedPort { + value: port as isize, + range: None, + }; } - _ => { - println!("Error receiving ports, using default values."); - (8002, 8003, 8004) + } else if media.media_name.media == "audio" { + if let Some(port) = media_info.audio_rtp_client { + media.media_name.port = RangedPort { + value: port as isize, + range: None, + }; } - }; - rtp_port = rtp_listen_port; - println!("=== Received RTPMAP: {} ===", rtp_port); - rtcp_port = rtp_server_port + 1; + } + } + let sdp = session.marshal(); + let mut file = File::create("output.sdp")?; + file.write_all(sdp.as_bytes())?; } - udp_socket - .connect(format!("127.0.0.1:{}", rtp_port)) - .await?; let child = Arc::new(create_child(args.command)?); defer!({ @@ -174,12 +198,19 @@ async fn main() -> Result<()> { } } }); + tokio::spawn(rtp_send( + video_recv, + host.clone(), + media_info.video_rtp_client, + media_info.video_rtp_server, + )); + tokio::spawn(rtp_send( + audio_recv, + host.clone(), + media_info.audio_rtp_client, + media_info.audio_rtp_server, + )); - tokio::spawn(async move { - while let Some(data) = recv.recv().await { - let _ = udp_socket.send(&data).await; - } - }); let wait_child = child.clone(); tokio::spawn(async move { match wait_child.as_ref() { @@ -197,58 +228,101 @@ async fn main() -> Result<()> { None => info!("No child process"), } }); - if args.mode == Mode::Rtsp { - tokio::spawn(rtcp_listener(args.host, rtcp_port, peer.clone())); + if input.scheme() == SCHEME_RTSP_SERVER { + tokio::spawn(rtcp_listener( + host.clone(), + media_info.video_rtp_server, + peer.clone(), + )); } tokio::select! { - _= complete_rx.recv() => { } + _ = complete_rx.recv() => { } msg = signal::wait_for_stop_signal() => warn!("Received signal: {}", msg) } + let _ = client.remove_resource().await; let _ = peer.close().await; + Ok(()) } -async fn rtcp_listener(host: String, rtcp_port: u16, peer: Arc) { - let rtcp_listener = UdpSocket::bind(format!("{}:{}", host, rtcp_port)) - .await - .unwrap(); - println!( - "RTCP listener bound to: {}", - rtcp_listener.local_addr().unwrap() - ); - let mut rtcp_buf = vec![0u8; 1500]; - - loop { - match rtcp_listener.recv_from(&mut rtcp_buf).await { - Ok((len, addr)) => { - if len > 0 { - debug!("Received {} bytes of RTCP data from {}", len, addr); - let mut rtcp_data = &rtcp_buf[..len]; - - match rtcp::packet::unmarshal(&mut rtcp_data) { - Ok(rtcp_packets) => { - for packet in rtcp_packets { - debug!("Received RTCP packet from {}: {:?}", addr, packet); - match peer.write_rtcp(&[packet]).await { - Ok(_) => { - debug!("Successfully sent RTCP packet to remote peer"); - } - Err(e) => { - error!("Error sending RTCP data to remote peer: {}", e); +async fn rtp_send( + mut receiver: UnboundedReceiver>, + host: String, + client_port: Option, + server_port: Option, +) { + if let Some(port) = client_port { + let server_addr = if let Some(server_port) = server_port { + format!("{}:{}", host, server_port) + } else { + "0.0.0.0:0".to_string() + }; + + let socket = match UdpSocket::bind(&server_addr).await { + Ok(s) => { + info!("UDP socket bound to {}", server_addr); + s + } + Err(e) => { + error!("Failed to bind UDP socket on {}: {}", server_addr, e); + return; + } + }; + let client_addr = format!("{}:{}", host, port); + + while let Some(data) = receiver.recv().await { + match socket.send_to(&data, &client_addr).await { + Ok(_) => debug!("Data sent to {}", client_addr), + Err(e) => error!("Failed to send data to {}: {}", client_addr, e), + } + } + } else { + } +} + +async fn rtcp_listener(host: String, rtcp_port: Option, peer: Arc) { + if let Some(rtcp_port) = rtcp_port { + let rtcp_listener = UdpSocket::bind(format!("{}:{}", host, rtcp_port + 1)) + .await + .unwrap(); + info!( + "RTCP listener bound to: {:?}", + rtcp_listener.local_addr().unwrap() + ); + let mut rtcp_buf = vec![0u8; 1500]; + + loop { + match rtcp_listener.recv_from(&mut rtcp_buf).await { + Ok((len, addr)) => { + if len > 0 { + debug!("Received {} bytes of RTCP data from {}", len, addr); + let mut rtcp_data = &rtcp_buf[..len]; + + match rtcp::packet::unmarshal(&mut rtcp_data) { + Ok(rtcp_packets) => { + for packet in rtcp_packets { + debug!("Received RTCP packet from {}: {:?}", addr, packet); + match peer.write_rtcp(&[packet]).await { + Ok(_) => { + debug!("Successfully sent RTCP packet to remote peer"); + } + Err(e) => { + error!("Error sending RTCP data to remote peer: {}", e); + } } } } - } - Err(e) => { - error!("Failed to parse RTCP packet: {}", e); + Err(e) => { + error!("Failed to parse RTCP packet: {}", e); + } } } } - } - Err(e) => { - error!("Error receiving RTCP data: {}", e); + Err(e) => { + error!("Error receiving RTCP data: {}", e); + } } } } @@ -256,29 +330,20 @@ async fn rtcp_listener(host: String, rtcp_port: u16, peer: Arc>, - payload_type: u8, + video_send: UnboundedSender>, + audio_send: UnboundedSender>, complete_tx: UnboundedSender<()>, + codec_info: Arc>, ) -> Result<(Arc, RTCSessionDescription)> { - let audio_payload_type = payload_type + 1; let peer = new_peer( - RTCRtpCodecParameters { - capability: codec, - payload_type, - stats_id: Default::default(), - }, - RTCRtpCodecParameters { - capability: audio_codec, - payload_type: audio_payload_type, - stats_id: Default::default(), - }, + video_send, + audio_send, complete_tx.clone(), - send, + codec_info.clone(), ) .await .map_err(|error| anyhow!(format!("[{}] {}", PREFIX_LIB, error)))?; + let offer = peer.create_offer(None).await?; let mut gather_complete = peer.gathering_complete_promise().await; @@ -290,6 +355,7 @@ async fn webrtc_start( .await?; debug!("Get http header link ice servers: {:?}", ice_servers); + let mut current_config = peer.get_configuration().await; current_config.ice_servers.clone_from(&ice_servers); peer.set_configuration(current_config.clone()).await?; @@ -302,18 +368,14 @@ async fn webrtc_start( } async fn new_peer( - video_codec: RTCRtpCodecParameters, - audio_codec: RTCRtpCodecParameters, + video_send: UnboundedSender>, + audio_send: UnboundedSender>, complete_tx: UnboundedSender<()>, - sender: UnboundedSender>, + codec_info: Arc>, ) -> Result> { - let video_ct = get_codec_type(&video_codec.capability); - let audio_ct = get_codec_type(&audio_codec.capability); let mut m = MediaEngine::default(); - m.register_codec(video_codec, video_ct)?; - - m.register_codec(audio_codec, audio_ct)?; + m.register_default_codecs()?; let mut registry = Registry::new(); registry = register_default_interceptors(registry, &mut m)?; @@ -338,34 +400,33 @@ async fn new_peer( .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?, ); - let _ = peer - .add_transceiver_from_kind( - video_ct, - Some(RTCRtpTransceiverInit { - direction: RTCRtpTransceiverDirection::Recvonly, - send_encodings: vec![], - }), - ) - .await - .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; - - let _ = peer - .add_transceiver_from_kind( - audio_ct, - Some(RTCRtpTransceiverInit { - direction: RTCRtpTransceiverDirection::Recvonly, - send_encodings: vec![], - }), - ) - .await - .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; + peer.add_transceiver_from_kind( + RTPCodecType::Video, + Some(RTCRtpTransceiverInit { + direction: RTCRtpTransceiverDirection::Recvonly, + send_encodings: vec![], + }), + ) + .await + .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; + + peer.add_transceiver_from_kind( + RTPCodecType::Audio, + Some(RTCRtpTransceiverInit { + direction: RTCRtpTransceiverDirection::Recvonly, + send_encodings: vec![], + }), + ) + .await + .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; let pc = peer.clone(); + peer.on_peer_connection_state_change(Box::new(move |s| { let pc = pc.clone(); let complete_tx = complete_tx.clone(); tokio::spawn(async move { - warn!("connection state changed: {}", s); + warn!("Connection state changed: {}", s); match s { RTCPeerConnectionState::Failed | RTCPeerConnectionState::Disconnected => { let _ = pc.close().await; @@ -379,18 +440,45 @@ async fn new_peer( Box::pin(async {}) })); - peer.on_track(Box::new(move |track, _, _| { - let sender = sender.clone(); - tokio::spawn(async move { - let mut b = [0u8; 1500]; - while let Ok((rtp_packet, _)) = track.read(&mut b).await { - trace!("received packet: {}", rtp_packet); - let size = rtp_packet.marshal_size(); - let data = b[0..size].to_vec(); - let _ = sender.send(data); + peer.on_track(Box::new({ + let codec_info = codec_info.clone(); + move |track, _, _| { + let video_sender = video_send.clone(); + let audio_sender = audio_send.clone(); + let codec = track.codec().clone(); + let track_kind = track.kind(); + + let codec_info = codec_info.clone(); + tokio::spawn(async move { + let mut codec_info = codec_info.lock().await; + if track_kind == RTPCodecType::Video { + debug!("Updating video codec info: {:?}", codec); + codec_info.video_codec = Some(codec.clone()); + } else if track_kind == RTPCodecType::Audio { + debug!("Updating audio codec info: {:?}", codec); + codec_info.audio_codec = Some(codec.clone()); + } + }); + + let sender = match track_kind { + RTPCodecType::Video => Some(video_sender), + RTPCodecType::Audio => Some(audio_sender), + _ => None, + }; + + if let Some(sender) = sender { + tokio::spawn(async move { + let mut b = [0u8; 1500]; + while let Ok((rtp_packet, _)) = track.read(&mut b).await { + trace!("Received RTP packet: {:?}", rtp_packet); + let size = rtp_packet.marshal_size(); + let data = b[0..size].to_vec(); + let _ = sender.send(data); + } + }); } - }); - Box::pin(async {}) + Box::pin(async {}) + } })); Ok(peer) diff --git a/tools/whipinto/src/main.rs b/tools/whipinto/src/main.rs index 34f50bff..5b25109f 100644 --- a/tools/whipinto/src/main.rs +++ b/tools/whipinto/src/main.rs @@ -28,8 +28,9 @@ use webrtc::{ util::Unmarshal, }; -use cli::{create_child, Codec}; +use cli::{codec_from_str, create_child, Codec}; use libwish::Client; +use tokio::sync::mpsc::UnboundedReceiver; use rtspclient::setup_rtsp_session; @@ -99,26 +100,22 @@ async fn main() -> Result<()> { Host::Ipv6(_) => Ipv6Addr::UNSPECIFIED.to_string(), }; - if let Some(h) = args.host { + if let Some(ref h) = args.host { debug!("=== Specified set host, using {} ===", h); - host = h; + host = h.clone(); } - let mut video_codec = Codec::Vp8; - let mut audio_codec = Codec::Opus; - let mut rtp_port = input.port().unwrap_or(0); - let mut audio_port = 0; - let mut rtcp_send_port = 0; + let video_port = input.port().unwrap_or(0); let (complete_tx, mut complete_rx) = unbounded_channel(); if input.scheme() == SCHEME_RTSP_SERVER { - let (tx, mut rx) = unbounded_channel::(); + let (tx, mut rx) = unbounded_channel::(); let mut handler = rtsp::Handler::new(tx, complete_tx.clone()); let host2 = host.to_string(); tokio::spawn(async move { - let listener = TcpListener::bind(format!("{}:{}", host2.clone(), rtp_port)) + let listener = TcpListener::bind(format!("{}:{}", host2.clone(), video_port)) .await .unwrap(); warn!( @@ -135,47 +132,36 @@ async fn main() -> Result<()> { } }); - // match rx.recv().await { - // Some(_rtpmap) => { - // //println!("=== Received RTPMAP: {} ===", rtpmap); - // //match rtpmap.split_once(' ') { - // // Some((pt, code)) => { - // // println!("=== Received PT: {} CODEC: {} ===", pt, code); - // // codec = match code { - // // "AV1/90000" => Codec::AV1, - // // "VP8/90000" => Codec::Vp8, - // // "VP9/90000" => Codec::Vp9, - // // "H264/90000" => Codec::H264, - // // _ => Codec::H264, - // // }; - // // } - // // None => {} - // //}; - // } - // None => { - // println!("=== No RTPMAP received ==="); - // } - // }; - - let (_rtp_listen_port, rtcp_listen_port, rtp_server_port) = - match (rx.recv().await, rx.recv().await, rx.recv().await) { - (Some(rtp), Some(rtcp), Some(rtp_server)) => { - let rtp_port = rtp.parse::().unwrap_or(8000); - let rtcp_port = rtcp.parse::().unwrap_or(8001); - let rtp_server_port = rtp_server.parse::().unwrap_or(8002); - (rtp_port, rtcp_port, rtp_server_port) - } - _ => { - println!("Error receiving ports, using default values."); - (8000, 8001, 8002) - } - }; - rtp_port = rtp_server_port; - rtcp_send_port = rtcp_listen_port; + while let Some(media_info) = rx.recv().await { + let _ = setup_webrtc_session( + &args, + media_info.video_codec, + media_info.audio_codec, + &complete_tx, + &mut complete_rx, + media_info.video_rtp_server, + media_info.audio_rtp_server, + media_info.video_rtcp_client, + host.clone(), + ) + .await; + } } else if input.scheme() == SCHEME_RTSP_CLIENT { - (rtp_port, audio_port, video_codec, audio_codec) = setup_rtsp_session(&args.input).await?; + let media_info = setup_rtsp_session(&args.input).await?; + let _ = setup_webrtc_session( + &args, + media_info.video_codec, + media_info.audio_codec, + &complete_tx, + &mut complete_rx, + media_info.video_rtp_client, + media_info.audio_rtp_client, + media_info.video_rtp_server, + host, + ) + .await; } else { - let sdp = sdp_types::Session::parse(&fs::read(args.input).unwrap()).unwrap(); + let sdp = sdp_types::Session::parse(&fs::read(&args.input).unwrap()).unwrap(); let video_track = sdp.medias.iter().find(|md| md.media == "video"); let audio_track = sdp.medias.iter().find(|md| md.media == "audio"); @@ -212,96 +198,34 @@ async fn main() -> Result<()> { }) .unwrap_or_else(|| "unknown".to_string()); - video_codec = rtspclient::codec_from_str(&codec_vid)?; - audio_codec = rtspclient::codec_from_str(&codec_aud)?; - rtp_port = video_track.unwrap().port; - audio_port = audio_track.unwrap().port; - } - - debug!("use rtp port {}", rtp_port); - let listener = UdpSocket::bind(format!("{}:{}", host, rtp_port)).await?; - let port = listener.local_addr()?.port(); - info!( - "=== RTP listener started : {} ===", - listener.local_addr().unwrap() - ); - - debug!("use audio_rtp port {}", audio_port); - let audio_listener = UdpSocket::bind(format!("{}:{}", host, audio_port)).await?; - let _audio_port = listener.local_addr()?.port(); - info!( - "=== audio_RTP listener started : {} ===", - audio_listener.local_addr().unwrap() - ); - let mut client = Client::new( - args.whip, - Client::get_auth_header_map(args.auth_basic, args.auth_token), - ); - let child = if let Some(command) = args.command { - let command = command.replace("{port}", &port.to_string()); - Arc::new(create_child(Some(command))?) - } else { - Default::default() - }; - defer!({ - if let Some(child) = child.as_ref() { - if let Ok(mut child) = child.lock() { - let _ = child.kill(); - } - } - }); - let (peer, video_sender, audio_sender) = webrtc_start( - &mut client, - video_codec.into(), - audio_codec.into(), - complete_tx.clone(), - ) - .await - .map_err(|error| anyhow!(format!("[{}] {}", PREFIX_LIB, error)))?; - - tokio::spawn(rtp_listener(listener, video_sender)); - tokio::spawn(rtp_listener(audio_listener, audio_sender)); - if input.scheme() == SCHEME_RTSP_SERVER { - let rtcp_port = rtp_port + 1; - tokio::spawn(rtcp_listener(host.clone(), rtcp_port, peer.clone())); - let senders = peer.get_senders().await; - for sender in senders { - tokio::spawn(read_rtcp(sender, host.clone(), rtcp_send_port)); - } + let video_codec = Some(codec_from_str(&codec_vid)?); + let audio_codec = Some(codec_from_str(&codec_aud)?); + let video_port = video_track.map(|track| track.port); + let audio_port = audio_track.map(|track| track.port); + let rtcp_send_port = None; + let _ = setup_webrtc_session( + &args, + video_codec, + audio_codec, + &complete_tx, + &mut complete_rx, + video_port, + audio_port, + rtcp_send_port, + host, + ) + .await; } - - let wait_child = child.clone(); - tokio::spawn(async move { - match wait_child.as_ref() { - Some(child) => loop { - if let Ok(mut child) = child.lock() { - if let Ok(wait) = child.try_wait() { - if wait.is_some() { - let _ = complete_tx.send(()); - return; - } - } - } - tokio::time::sleep(Duration::from_secs(1)).await; - }, - None => info!("No child process"), - } - }); - tokio::select! { - _= complete_rx.recv() => { } - msg = signal::wait_for_stop_signal() => warn!("Received signal: {}", msg) - } - warn!("RTP listener closed"); - let _ = client.remove_resource().await; - let _ = peer.close().await; Ok(()) } -async fn rtp_listener(socker: UdpSocket, sender: UnboundedSender>) { - let mut inbound_rtp_packet = vec![0u8; 1600]; - while let Ok((n, _)) = socker.recv_from(&mut inbound_rtp_packet).await { - let data = inbound_rtp_packet[..n].to_vec(); - let _ = sender.send(data); +async fn rtp_listener(socker: UdpSocket, sender: Option>>) { + if let Some(sender) = sender { + let mut inbound_rtp_packet = vec![0u8; 1600]; + while let Ok((n, _)) = socker.recv_from(&mut inbound_rtp_packet).await { + let data = inbound_rtp_packet[..n].to_vec(); + let _ = sender.send(data); + } } } @@ -334,7 +258,7 @@ async fn rtcp_listener(host: String, rtcp_port: u16, peer: Arc, host: String, port: u16) -> Result<()> { - let udp_socket = UdpSocket::bind(format!("{}:{}", host, port)).await.unwrap(); + let udp_socket = UdpSocket::bind(format!("{}:0", host)).await?; loop { match sender.read_rtcp().await { @@ -347,10 +271,12 @@ async fn read_rtcp(sender: Arc, host: String, port: u16) -> Result buf.extend_from_slice(&serialized_packet); } if !buf.is_empty() { - if let Err(err) = udp_socket.send(&buf).await { + if let Err(err) = + udp_socket.send_to(&buf, format!("{}:{}", host, port)).await + { warn!("Failed to forward RTCP packet: {}", err); } else { - debug!("Forwarded RTCP packet to {}", port); + debug!("Forwarded RTCP packet to {}:{}", host, port); } } } @@ -362,18 +288,127 @@ async fn read_rtcp(sender: Arc, host: String, port: u16) -> Result } } +async fn setup_webrtc_session( + args: &Args, + video_codec: Option, + audio_codec: Option, + complete_tx: &UnboundedSender<()>, + complete_rx: &mut UnboundedReceiver<()>, + video_port: Option, + audio_port: Option, + rtcp_sender_port: Option, + host: String, +) -> Result<()> { + let mut client = Client::new( + args.whip.clone(), + Client::get_auth_header_map(args.auth_basic.clone(), args.auth_token.clone()), + ); + + let child = if let Some(command) = &args.command { + let command = command.replace("{port}", "0"); + Arc::new(create_child(Some(command))?) + } else { + Default::default() + }; + + defer!({ + if let Some(child) = child.as_ref() { + if let Ok(mut child) = child.lock() { + let _ = child.kill(); + } + } + }); + + let (peer, video_sender, audio_sender) = webrtc_start( + &mut client, + video_codec.map(|c| c.into()), + audio_codec.map(|c| c.into()), + complete_tx.clone(), + args, + ) + .await + .map_err(|error| anyhow!(format!("[{}] {}", PREFIX_LIB, error)))?; + + if let Some(video_port) = video_port { + let video_listener = UdpSocket::bind(format!("{}:{}", host, video_port)).await?; + info!( + "=== video listener started : {} ===", + video_listener.local_addr().unwrap() + ); + tokio::spawn(rtp_listener(video_listener, video_sender)); + } else { + info!("Video codec is None, skipping video transmission."); + } + + if let Some(audio_port) = audio_port { + let audio_listener = UdpSocket::bind(format!("{}:{}", host, audio_port)).await?; + info!( + "=== audio listener started : {} ===", + audio_listener.local_addr().unwrap() + ); + tokio::spawn(rtp_listener(audio_listener, audio_sender)); + } else { + info!("Audio codec is None, skipping audio transmission."); + } + + let rtcp_listener_port = video_port.unwrap_or(0) + 1; + tokio::spawn(rtcp_listener( + host.clone(), + rtcp_listener_port, + peer.clone(), + )); + let senders = peer.get_senders().await; + if let Some(rtcp_sender_port) = rtcp_sender_port { + for sender in senders { + tokio::spawn(read_rtcp(sender, host.clone(), rtcp_sender_port)); + } + } + + let wait_child = child.clone(); + match wait_child.as_ref() { + Some(child) => loop { + if let Ok(mut child) = child.lock() { + if let Ok(wait) = child.try_wait() { + if wait.is_some() { + let _ = complete_tx.send(()); + return Ok(()); + } + } + } + tokio::time::sleep(Duration::from_secs(1)).await; + }, + None => info!("No child process"), + } + + tokio::select! { + _ = complete_rx.recv() => {} + msg = signal::wait_for_stop_signal() => {warn!("Received signal: {}", msg)} + } + warn!("RTP listener closed"); + let _ = client.remove_resource().await; + let _ = peer.close().await; + + Ok(()) +} + async fn webrtc_start( client: &mut Client, - video_codec: RTCRtpCodecCapability, - audio_codec: RTCRtpCodecCapability, + video_codec: Option, + audio_codec: Option, complete_tx: UnboundedSender<()>, + args: &Args, ) -> Result<( Arc, - UnboundedSender>, - UnboundedSender>, + Option>>, + Option>>, )> { - let (peer, video_sender, audio_sender) = - new_peer(video_codec, audio_codec, complete_tx.clone()).await?; + let (peer, video_sender, audio_sender) = new_peer( + video_codec, + audio_codec, + complete_tx.clone(), + args.input.clone(), + ) + .await?; let offer = peer.create_offer(None).await?; let mut gather_complete = peer.gathering_complete_promise().await; @@ -397,13 +432,14 @@ async fn webrtc_start( } async fn new_peer( - video_codec: RTCRtpCodecCapability, - audio_codec: RTCRtpCodecCapability, + video_codec: Option, + audio_codec: Option, complete_tx: UnboundedSender<()>, + input: String, ) -> Result<( Arc, - UnboundedSender>, - UnboundedSender>, + Option>>, + Option>>, )> { let mut m = MediaEngine::default(); m.register_default_codecs()?; @@ -436,7 +472,7 @@ async fn new_peer( let pc = pc.clone(); let complete_tx = complete_tx.clone(); tokio::spawn(async move { - warn!("connection state changed: {}", s); + warn!("Connection state changed: {}", s); match s { RTCPeerConnectionState::Failed | RTCPeerConnectionState::Disconnected => { let _ = pc.close().await; @@ -449,68 +485,84 @@ async fn new_peer( }); Box::pin(async {}) })); - let video_mime_type = video_codec.mime_type.clone(); - let video_track = Arc::new(TrackLocalStaticRTP::new( - video_codec, - "webrtc-video".to_owned(), - "webrtc-video-rs".to_owned(), - )); - let _ = peer - .add_track(video_track.clone() as Arc) - .await - .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; - let (video_tx, mut video_rx) = unbounded_channel::>(); - let audio_mime_type = audio_codec.mime_type.clone(); - let audio_track = Arc::new(TrackLocalStaticRTP::new( - audio_codec, - "webrtc-audio".to_owned(), - "webrtc-audio-rs".to_owned(), - )); - let _ = peer - .add_track(audio_track.clone() as Arc) - .await - .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; - let (audio_tx, mut audio_rx) = unbounded_channel::>(); - - tokio::spawn(async move { - debug!("Codec is: {}", video_mime_type); - let mut handler: Box = match video_mime_type.as_str() { - MIME_TYPE_VP8 => Box::new(payload::RePayloadCodec::new(video_mime_type)), - MIME_TYPE_VP9 => Box::new(payload::RePayloadCodec::new(video_mime_type)), - MIME_TYPE_H264 => Box::new(payload::RePayloadCodec::new(video_mime_type)), - _ => Box::new(payload::Forward::new()), - }; - - while let Some(data) = video_rx.recv().await { - if let Ok(packet) = Packet::unmarshal(&mut data.as_slice()) { - trace!("received packet: {}", packet); - for packet in handler.payload(packet) { - trace!("send packet: {}", packet); - let _ = video_track.write_rtp(&packet).await; + let video_tx = if let Some(video_codec) = video_codec { + let video_track_id = format!("{}-video", input); + let video_track = Arc::new(TrackLocalStaticRTP::new( + video_codec.clone(), + video_track_id.to_owned(), + input.to_owned(), + )); + let _ = peer + .add_track(video_track.clone() as Arc) + .await + .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; + + let (video_tx, mut video_rx) = unbounded_channel::>(); + tokio::spawn(async move { + debug!("Video codec: {}", video_codec.mime_type); + let mut handler: Box = + match video_codec.mime_type.as_str() { + MIME_TYPE_VP8 => Box::new(payload::RePayloadCodec::new(video_codec.mime_type)), + MIME_TYPE_VP9 => Box::new(payload::RePayloadCodec::new(video_codec.mime_type)), + MIME_TYPE_H264 => Box::new(payload::RePayloadCodec::new(video_codec.mime_type)), + _ => Box::new(payload::Forward::new()), + }; + + while let Some(data) = video_rx.recv().await { + if let Ok(packet) = Packet::unmarshal(&mut data.as_slice()) { + trace!("Received video packet: {}", packet); + for packet in handler.payload(packet) { + trace!("Sending video packet: {}", packet); + let _ = video_track.write_rtp(&packet).await; + } } } - } - }); + }); + Some(video_tx) + } else { + None + }; + + let audio_tx = if let Some(audio_codec) = audio_codec { + let audio_track_id = format!("{}-audio", input); + let audio_track = Arc::new(TrackLocalStaticRTP::new( + audio_codec.clone(), + audio_track_id.to_owned(), + input.to_owned(), + )); + let _ = peer + .add_track(audio_track.clone() as Arc) + .await + .map_err(|error| anyhow!(format!("{:?}: {}", error, error)))?; + + let (audio_tx, mut audio_rx) = unbounded_channel::>(); + tokio::spawn(async move { + debug!("Audio codec: {}", audio_codec.mime_type); + let mut handler: Box = + match audio_codec.mime_type.as_str() { + MIME_TYPE_OPUS => { + Box::new(payload::RePayloadCodec::new(audio_codec.mime_type.clone())) + } + _ => Box::new(payload::Forward::new()), + }; - tokio::spawn(async move { - debug!("Codec is: {}", audio_mime_type); - let mut handler: Box = match audio_mime_type.as_str() { - MIME_TYPE_OPUS => Box::new(payload::RePayloadCodec::new(audio_mime_type.clone())), - _ => Box::new(payload::Forward::new()), - }; - - while let Some(data) = audio_rx.recv().await { - if audio_mime_type == MIME_TYPE_G722 { - let _ = audio_track.write(&data).await; - } else if let Ok(packet) = Packet::unmarshal(&mut data.as_slice()) { - trace!("received packet: {}", packet); - for packet in handler.payload(packet) { - trace!("send packet: {}", packet); - let _ = audio_track.write_rtp(&packet).await; + while let Some(data) = audio_rx.recv().await { + if audio_codec.mime_type == MIME_TYPE_G722 { + let _ = audio_track.write(&data).await; + } else if let Ok(packet) = Packet::unmarshal(&mut data.as_slice()) { + trace!("Received audio packet: {}", packet); + for packet in handler.payload(packet) { + trace!("Sending audio packet: {}", packet); + let _ = audio_track.write_rtp(&packet).await; + } } } - } - }); + }); + Some(audio_tx) + } else { + None + }; + Ok((peer, video_tx, audio_tx)) } diff --git a/tools/whipinto/src/rtspclient.rs b/tools/whipinto/src/rtspclient.rs index a9717558..77f22764 100644 --- a/tools/whipinto/src/rtspclient.rs +++ b/tools/whipinto/src/rtspclient.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Ok, Result}; -use cli::Codec; +use cli::codec_from_str; use md5::{Digest, Md5}; use portpicker::pick_unused_port; use rtsp_types::{ @@ -33,19 +33,6 @@ struct RtspSession { auth_header: Option, } -pub fn codec_from_str(s: &str) -> Result { - match s { - "VP8" => Ok(Codec::Vp8), - "VP9" => Ok(Codec::Vp9), - "H264" => Ok(Codec::H264), - "AV1" => Ok(Codec::AV1), - "OPUS" => Ok(Codec::Opus), - "G722" => Ok(Codec::G722), - // _ => Err(anyhow!("Unknown codec: {}", s)), - _ => Ok(Codec::G722), - } -} - impl RtspSession { async fn send_request(&mut self, request: &Request>) -> Result<()> { let mut buffer = Vec::new(); @@ -296,7 +283,7 @@ impl RtspSession { } } -pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Codec)> { +pub async fn setup_rtsp_session(rtsp_url: &str) -> Result { let mut url = Url::parse(rtsp_url)?; let host = url .host() @@ -339,8 +326,8 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Code return Err(anyhow!("No tracks found in SDP")); } - let rtp_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; - let rtp_audio_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; + let video_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; + let audio_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; let video_uri = video_track .and_then(|md| { @@ -380,7 +367,7 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Code trace!("audio uri: {:?}", audio_uri); rtsp_session.uri.clone_from(&video_uri); - rtsp_session.rtp_client_port = Some(rtp_port); + rtsp_session.rtp_client_port = Some(video_port); let session_id = rtsp_session.send_setup_request().await?; trace!("session id: {:?}", session_id); @@ -388,7 +375,7 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Code rtsp_session.session_id = Some(session_id); rtsp_session.uri.clone_from(&audio_uri); - rtsp_session.rtp_client_port = Some(rtp_audio_port); + rtsp_session.rtp_client_port = Some(audio_port); rtsp_session.send_setup_request().await?; let play_request = Request::builder(Method::Play, Version::V1_0) @@ -441,7 +428,7 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Code } }) }) - .unwrap_or_else(|| "unknown".to_string()); + .and_then(|codec_str| codec_from_str(&codec_str).ok()); let audio_codec = audio_track .and_then(|md| { @@ -458,10 +445,17 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Code } }) }) - .unwrap_or_else(|| "unknown".to_string()); - - let video_codec = codec_from_str(&video_codec)?; - let audio_codec = codec_from_str(&audio_codec)?; + .and_then(|codec_str| codec_from_str(&codec_str).ok()); + + let media_info = rtsp::MediaInfo { + video_rtp_client: Some(video_port), + video_rtcp_client: Some(video_port + 1), + video_rtp_server: None, + audio_rtp_client: Some(audio_port), + audio_rtp_server: None, + video_codec, + audio_codec, + }; - Ok((rtp_port, rtp_audio_port, video_codec, audio_codec)) + Ok(media_info) } diff --git a/tools/whipinto/test.sdp b/tools/whipinto/test.sdp new file mode 100644 index 00000000..1ad0b418 --- /dev/null +++ b/tools/whipinto/test.sdp @@ -0,0 +1,13 @@ +v=0 +o=- 0 0 IN IP4 127.0.0.1 +s=No Name +t=0 0 +a=tool:libavformat 61.3.100 +m=video 8004 RTP/AVP 96 +c=IN IP4 192.168.31.243 +b=AS:500 +a=rtpmap:96 VP8/90000 +m=audio 8006 RTP/AVP 97 +c=IN IP4 192.168.31.243 +b=AS:64 +a=rtpmap:97 G722/8000/1 \ No newline at end of file From 9aad5249660307403783328bac13075d3c4d9990 Mon Sep 17 00:00:00 2001 From: Marsyew Date: Sun, 29 Sep 2024 19:56:10 +0800 Subject: [PATCH 03/15] fix cargo fmt --- libs/rtsp/src/lib.rs | 260 +++++++++++++++---------------- tools/whepfrom/src/main.rs | 3 +- tools/whipinto/src/main.rs | 88 +++++------ tools/whipinto/src/rtspclient.rs | 207 +++++++++++++++++++++--- 4 files changed, 349 insertions(+), 209 deletions(-) diff --git a/libs/rtsp/src/lib.rs b/libs/rtsp/src/lib.rs index 4f2f7e99..8158b219 100644 --- a/libs/rtsp/src/lib.rs +++ b/libs/rtsp/src/lib.rs @@ -3,10 +3,7 @@ use cli::{codec_from_str, Codec}; use portpicker::pick_unused_port; use rtsp_types::ParseError; use rtsp_types::{headers, headers::transport, Message, Method, Request, Response, StatusCode}; -use sdp::{ - description::common::Attribute, - SessionDescription, -}; +use sdp::{description::common::Attribute, SessionDescription}; use sdp_types::Session; use std::io::Cursor; use tokio::{ @@ -25,7 +22,7 @@ pub struct Handler { up_tx: UnboundedSender, dn_tx: UnboundedSender<()>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct MediaInfo { pub video_rtp_client: Option, pub video_rtcp_client: Option, @@ -51,17 +48,9 @@ impl CodecInfo { } } -impl Default for MediaInfo { +impl Default for CodecInfo { fn default() -> Self { - Self { - video_rtp_client: None, - video_rtcp_client: None, - video_rtp_server: None, - audio_rtp_client: None, - audio_rtp_server: None, - video_codec: None, - audio_codec: None, - } + Self::new() } } @@ -91,8 +80,8 @@ impl Handler { video_rtp_server: self.media_info.video_rtp_server, audio_rtp_client: self.media_info.audio_rtp_client, audio_rtp_server: self.media_info.audio_rtp_server, - video_codec: self.media_info.video_codec.clone(), - audio_codec: self.media_info.audio_codec.clone(), + video_codec: self.media_info.video_codec, + audio_codec: self.media_info.audio_codec, }) .unwrap(); @@ -110,8 +99,8 @@ impl Handler { video_rtp_server: self.media_info.video_rtp_server, audio_rtp_client: self.media_info.audio_rtp_client, audio_rtp_server: self.media_info.audio_rtp_server, - video_codec: self.media_info.video_codec.clone(), - audio_codec: self.media_info.audio_codec.clone(), + video_codec: self.media_info.video_codec, + audio_codec: self.media_info.audio_codec, }) .unwrap(); @@ -141,129 +130,126 @@ impl Handler { .unwrap(); let tr = trs.first().unwrap(); - match tr { - transport::Transport::Rtp(rtp_transport) => { - let (rtp, rtcp) = rtp_transport.params.client_port.unwrap(); - let uri = req.request_uri().unwrap().as_str(); - - let url_id = uri - .split("streamid=") - .nth(1) - .and_then(|id_str| id_str.split('&').next()) - .map(|id| id.to_string()); - - if let Some(sdp_data) = &self.sdp { - let sdp = sdp_types::Session::parse(sdp_data).unwrap(); - - for media in sdp.medias.iter() { - let media_control = media + if let transport::Transport::Rtp(rtp_transport) = tr { + let (rtp, rtcp) = rtp_transport.params.client_port.unwrap(); + let uri = req.request_uri().unwrap().as_str(); + + let url_id = uri + .split("streamid=") + .nth(1) + .and_then(|id_str| id_str.split('&').next()) + .map(|id| id.to_string()); + + if let Some(sdp_data) = &self.sdp { + let sdp = sdp_types::Session::parse(sdp_data).unwrap(); + + for media in sdp.medias.iter() { + let media_control = media + .attributes + .iter() + .find(|attr| attr.attribute == "control") + .and_then(|attr| attr.value.as_deref()) + .and_then(|control| { + if control.contains("streamid=") { + control.split("streamid=").nth(1).map(|id| id.to_string()) + } else { + None + } + }); + + if media.media == "audio" && media_control.as_deref() == url_id.as_deref() { + let audio_server_port = + pick_unused_port().expect("Failed to find an unused audio port"); + let audio_rtcp_server_port = audio_server_port + 1; + + self.media_info.audio_rtp_client = Some(rtp); + self.media_info.audio_rtp_server = Some(audio_server_port); + self.media_info.audio_codec = media + .attributes + .iter() + .find(|attr| attr.attribute == "rtpmap") + .and_then(|attr| attr.value.as_ref()) + .and_then(|value| { + value + .split_whitespace() + .nth(1) + .unwrap_or("") + .split('/') + .next() + .map(|codec_str| codec_from_str(codec_str).ok()) + }) + .unwrap_or(None); + + return Response::builder(req.version(), StatusCode::Ok) + .header(headers::CSEQ, req.header(&headers::CSEQ).unwrap().as_str()) + .header(headers::SERVER, SERVER_NAME) + .header(headers::SESSION, "1111-2222-3333-4444") + .typed_header(&transport::Transports::from(vec![ + transport::Transport::Rtp(transport::RtpTransport { + profile: transport::RtpProfile::Avp, + lower_transport: None, + params: transport::RtpTransportParameters { + unicast: true, + server_port: Some(( + audio_server_port, + Some(audio_rtcp_server_port), + )), + ..Default::default() + }, + }), + ])) + .build(Vec::new()); + } else if media.media == "video" + && media_control.as_deref() == url_id.as_deref() + { + let video_server_port = + pick_unused_port().expect("Failed to find an unused video port"); + let video_rtcp_server_port = video_server_port + 1; + + self.media_info.video_rtp_client = Some(rtp); + self.media_info.video_rtcp_client = rtcp; + self.media_info.video_rtp_server = Some(video_server_port); + self.media_info.video_codec = media .attributes .iter() - .find(|attr| attr.attribute == "control") - .and_then(|attr| attr.value.as_deref()) - .and_then(|control| { - if control.contains("streamid=") { - control.split("streamid=").nth(1).map(|id| id.to_string()) - } else { - None - } - }); - - if media.media == "audio" && media_control.as_deref() == url_id.as_deref() { - let audio_server_port = - pick_unused_port().expect("Failed to find an unused audio port"); - let audio_rtcp_server_port = audio_server_port + 1; - - self.media_info.audio_rtp_client = Some(rtp); - self.media_info.audio_rtp_server = Some(audio_server_port); - self.media_info.audio_codec = media - .attributes - .iter() - .find(|attr| attr.attribute == "rtpmap") - .and_then(|attr| attr.value.as_ref()) - .and_then(|value| { - value - .split_whitespace() - .nth(1) - .unwrap_or("") - .split('/') - .next() - .map(|codec_str| codec_from_str(codec_str).ok()) - }) - .unwrap_or(None); - - return Response::builder(req.version(), StatusCode::Ok) - .header(headers::CSEQ, req.header(&headers::CSEQ).unwrap().as_str()) - .header(headers::SERVER, SERVER_NAME) - .header(headers::SESSION, "1111-2222-3333-4444") - .typed_header(&transport::Transports::from(vec![ - transport::Transport::Rtp(transport::RtpTransport { - profile: transport::RtpProfile::Avp, - lower_transport: None, - params: transport::RtpTransportParameters { - unicast: true, - server_port: Some(( - audio_server_port, - Some(audio_rtcp_server_port), - )), - ..Default::default() - }, - }), - ])) - .build(Vec::new()); - } else if media.media == "video" - && media_control.as_deref() == url_id.as_deref() - { - let video_server_port = - pick_unused_port().expect("Failed to find an unused video port"); - let video_rtcp_server_port = video_server_port + 1; - - self.media_info.video_rtp_client = Some(rtp); - self.media_info.video_rtcp_client = rtcp; - self.media_info.video_rtp_server = Some(video_server_port); - self.media_info.video_codec = media - .attributes - .iter() - .find(|attr| attr.attribute == "rtpmap") - .and_then(|attr| attr.value.as_ref()) - .and_then(|value| { - value - .split_whitespace() - .nth(1) - .unwrap_or("") - .split('/') - .next() - .map(|codec_str| codec_from_str(codec_str).ok()) - }) - .unwrap_or(None); - - return Response::builder(req.version(), StatusCode::Ok) - .header(headers::CSEQ, req.header(&headers::CSEQ).unwrap().as_str()) - .header(headers::SERVER, SERVER_NAME) - .header(headers::SESSION, "1111-2222-3333-4444") - .typed_header(&transport::Transports::from(vec![ - transport::Transport::Rtp(transport::RtpTransport { - profile: transport::RtpProfile::Avp, - lower_transport: None, - params: transport::RtpTransportParameters { - unicast: true, - server_port: Some(( - video_server_port, - Some(video_rtcp_server_port), - )), - ..Default::default() - }, - }), - ])) - .build(Vec::new()); - } + .find(|attr| attr.attribute == "rtpmap") + .and_then(|attr| attr.value.as_ref()) + .and_then(|value| { + value + .split_whitespace() + .nth(1) + .unwrap_or("") + .split('/') + .next() + .map(|codec_str| codec_from_str(codec_str).ok()) + }) + .unwrap_or(None); + + return Response::builder(req.version(), StatusCode::Ok) + .header(headers::CSEQ, req.header(&headers::CSEQ).unwrap().as_str()) + .header(headers::SERVER, SERVER_NAME) + .header(headers::SESSION, "1111-2222-3333-4444") + .typed_header(&transport::Transports::from(vec![ + transport::Transport::Rtp(transport::RtpTransport { + profile: transport::RtpProfile::Avp, + lower_transport: None, + params: transport::RtpTransportParameters { + unicast: true, + server_port: Some(( + video_server_port, + Some(video_rtcp_server_port), + )), + ..Default::default() + }, + }), + ])) + .build(Vec::new()); } - println!("Updated self.media_info: {:?}", self.media_info); - } else { - println!("SDP data is not available"); } + println!("Updated self.media_info: {:?}", self.media_info); + } else { + println!("SDP data is not available"); } - _ => {} } Response::builder(req.version(), StatusCode::Ok) diff --git a/tools/whepfrom/src/main.rs b/tools/whepfrom/src/main.rs index dada376d..f262e5a3 100644 --- a/tools/whepfrom/src/main.rs +++ b/tools/whepfrom/src/main.rs @@ -96,7 +96,7 @@ async fn main() -> Result<()> { if let Some(ref h) = args.host { debug!("=== Specified set host, using {} ===", h); - host = h.clone(); + host.clone_from(h); } let (complete_tx, mut complete_rx) = unbounded_channel(); @@ -278,7 +278,6 @@ async fn rtp_send( Err(e) => error!("Failed to send data to {}: {}", client_addr, e), } } - } else { } } diff --git a/tools/whipinto/src/main.rs b/tools/whipinto/src/main.rs index 5b25109f..8e95558f 100644 --- a/tools/whipinto/src/main.rs +++ b/tools/whipinto/src/main.rs @@ -28,7 +28,7 @@ use webrtc::{ util::Unmarshal, }; -use cli::{codec_from_str, create_child, Codec}; +use cli::{codec_from_str, create_child}; use libwish::Client; use tokio::sync::mpsc::UnboundedReceiver; @@ -102,7 +102,7 @@ async fn main() -> Result<()> { if let Some(ref h) = args.host { debug!("=== Specified set host, using {} ===", h); - host = h.clone(); + host.clone_from(h) } let video_port = input.port().unwrap_or(0); @@ -135,31 +135,27 @@ async fn main() -> Result<()> { while let Some(media_info) = rx.recv().await { let _ = setup_webrtc_session( &args, - media_info.video_codec, - media_info.audio_codec, &complete_tx, &mut complete_rx, - media_info.video_rtp_server, - media_info.audio_rtp_server, - media_info.video_rtcp_client, + &media_info, host.clone(), ) .await; } } else if input.scheme() == SCHEME_RTSP_CLIENT { - let media_info = setup_rtsp_session(&args.input).await?; - let _ = setup_webrtc_session( - &args, - media_info.video_codec, - media_info.audio_codec, - &complete_tx, - &mut complete_rx, - media_info.video_rtp_client, - media_info.audio_rtp_client, - media_info.video_rtp_server, - host, - ) - .await; + let (rtp_port, audio_port, video_codec, audio_codec) = + setup_rtsp_session(&args.input).await?; + let media_info = rtsp::MediaInfo { + video_rtp_client: None, + audio_rtp_client: None, + video_codec: Some(video_codec), + audio_codec: Some(audio_codec), + video_rtp_server: Some(rtp_port), + audio_rtp_server: Some(audio_port), + video_rtcp_client: Some(rtp_port + 1), + }; + let _ = + setup_webrtc_session(&args, &complete_tx, &mut complete_rx, &media_info, host).await; } else { let sdp = sdp_types::Session::parse(&fs::read(&args.input).unwrap()).unwrap(); let video_track = sdp.medias.iter().find(|md| md.media == "video"); @@ -197,24 +193,18 @@ async fn main() -> Result<()> { }) }) .unwrap_or_else(|| "unknown".to_string()); - - let video_codec = Some(codec_from_str(&codec_vid)?); - let audio_codec = Some(codec_from_str(&codec_aud)?); - let video_port = video_track.map(|track| track.port); - let audio_port = audio_track.map(|track| track.port); - let rtcp_send_port = None; - let _ = setup_webrtc_session( - &args, - video_codec, - audio_codec, - &complete_tx, - &mut complete_rx, - video_port, - audio_port, - rtcp_send_port, - host, - ) - .await; + let media_info = rtsp::MediaInfo { + video_rtp_client: None, + audio_rtp_client: None, + video_codec: Some(codec_from_str(&codec_vid)?), + audio_codec: Some(codec_from_str(&codec_aud)?), + video_rtp_server: video_track.map(|track| track.port), + audio_rtp_server: audio_track.map(|track| track.port), + video_rtcp_client: None, + }; + + let _ = + setup_webrtc_session(&args, &complete_tx, &mut complete_rx, &media_info, host).await; } Ok(()) } @@ -290,13 +280,9 @@ async fn read_rtcp(sender: Arc, host: String, port: u16) -> Result async fn setup_webrtc_session( args: &Args, - video_codec: Option, - audio_codec: Option, complete_tx: &UnboundedSender<()>, complete_rx: &mut UnboundedReceiver<()>, - video_port: Option, - audio_port: Option, - rtcp_sender_port: Option, + media_info: &rtsp::MediaInfo, host: String, ) -> Result<()> { let mut client = Client::new( @@ -321,15 +307,15 @@ async fn setup_webrtc_session( let (peer, video_sender, audio_sender) = webrtc_start( &mut client, - video_codec.map(|c| c.into()), - audio_codec.map(|c| c.into()), + media_info.video_codec.map(|c| c.into()), + media_info.audio_codec.map(|c| c.into()), complete_tx.clone(), args, ) .await .map_err(|error| anyhow!(format!("[{}] {}", PREFIX_LIB, error)))?; - if let Some(video_port) = video_port { + if let Some(video_port) = media_info.video_rtp_server { let video_listener = UdpSocket::bind(format!("{}:{}", host, video_port)).await?; info!( "=== video listener started : {} ===", @@ -337,10 +323,10 @@ async fn setup_webrtc_session( ); tokio::spawn(rtp_listener(video_listener, video_sender)); } else { - info!("Video codec is None, skipping video transmission."); + info!("Video stream is None"); } - if let Some(audio_port) = audio_port { + if let Some(audio_port) = media_info.audio_rtp_server { let audio_listener = UdpSocket::bind(format!("{}:{}", host, audio_port)).await?; info!( "=== audio listener started : {} ===", @@ -348,17 +334,17 @@ async fn setup_webrtc_session( ); tokio::spawn(rtp_listener(audio_listener, audio_sender)); } else { - info!("Audio codec is None, skipping audio transmission."); + info!("Audio stream is None."); } - let rtcp_listener_port = video_port.unwrap_or(0) + 1; + let rtcp_listener_port = media_info.video_rtp_server.unwrap_or(0) + 1; tokio::spawn(rtcp_listener( host.clone(), rtcp_listener_port, peer.clone(), )); let senders = peer.get_senders().await; - if let Some(rtcp_sender_port) = rtcp_sender_port { + if let Some(rtcp_sender_port) = media_info.video_rtcp_client { for sender in senders { tokio::spawn(read_rtcp(sender, host.clone(), rtcp_sender_port)); } diff --git a/tools/whipinto/src/rtspclient.rs b/tools/whipinto/src/rtspclient.rs index 77f22764..08c9966a 100644 --- a/tools/whipinto/src/rtspclient.rs +++ b/tools/whipinto/src/rtspclient.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Ok, Result}; -use cli::codec_from_str; +use cli::{codec_from_str, Codec}; use md5::{Digest, Md5}; use portpicker::pick_unused_port; use rtsp_types::{ @@ -283,7 +283,183 @@ impl RtspSession { } } -pub async fn setup_rtsp_session(rtsp_url: &str) -> Result { +// pub async fn setup_rtsp_session(rtsp_url: &str) -> Result { +// let mut url = Url::parse(rtsp_url)?; +// let host = url +// .host() +// .ok_or_else(|| anyhow!("Host not found"))? +// .to_string(); +// let port = url.port().unwrap_or(DEFAULT_RTSP_PORT); + +// let addr = format!("{}:{}", host, port); +// info!("Connecting to RTSP server at {}", addr); +// let stream = TcpStream::connect(addr).await?; + +// let mut rtsp_session = RtspSession { +// stream, +// uri: url.as_str().to_string(), +// cseq: 1, +// auth_params: AuthParams { +// username: url.username().to_string(), +// password: url.password().unwrap_or("").to_string(), +// }, +// session_id: None, +// rtp_client_port: None, +// auth_header: None, +// }; + +// url.set_username("").unwrap(); +// url.set_password(None).unwrap(); + +// rtsp_session.send_options_request().await?; + +// let sdp_content = rtsp_session.send_describe_request().await?; + +// let sdp: Session = Session::parse(sdp_content.as_bytes()) +// .map_err(|e| anyhow!("Failed to parse SDP: {}", e))?; + +// let video_track = sdp.medias.iter().find(|md| md.media == "video"); +// let audio_track = sdp.medias.iter().find(|md| md.media == "audio"); +// debug!("track video: {:?}, audio: {:?}", video_track, audio_track); + +// if video_track.is_none() && audio_track.is_none() { +// return Err(anyhow!("No tracks found in SDP")); +// } + +// let video_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; +// let audio_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; + +// let video_uri = video_track +// .and_then(|md| { +// md.attributes.iter().find_map(|attr| { +// if attr.attribute == "control" { +// let value = attr.value.clone().unwrap_or_default(); +// if value.starts_with("rtsp://") { +// Some(value) +// } else { +// Some(format!("{}/{}", rtsp_session.uri, value)) +// } +// } else { +// None +// } +// }) +// }) +// .unwrap_or_else(|| format!("{}/trackID=1", rtsp_session.uri)); + +// let audio_uri = audio_track +// .and_then(|md| { +// md.attributes.iter().find_map(|attr| { +// if attr.attribute == "control" { +// let value = attr.value.clone().unwrap_or_default(); +// if value.starts_with("rtsp://") { +// Some(value) +// } else { +// Some(format!("{}/{}", rtsp_session.uri, value)) +// } +// } else { +// None +// } +// }) +// }) +// .unwrap_or_else(|| format!("{}/trackID=2", rtsp_session.uri)); + +// trace!("video uri: {:?}", video_uri); +// trace!("audio uri: {:?}", audio_uri); + +// rtsp_session.uri.clone_from(&video_uri); +// rtsp_session.rtp_client_port = Some(video_port); + +// let session_id = rtsp_session.send_setup_request().await?; +// trace!("session id: {:?}", session_id); + +// rtsp_session.session_id = Some(session_id); + +// rtsp_session.uri.clone_from(&audio_uri); +// rtsp_session.rtp_client_port = Some(audio_port); +// rtsp_session.send_setup_request().await?; + +// let play_request = Request::builder(Method::Play, Version::V1_0) +// .request_uri( +// rtsp_session +// .uri +// .parse::() +// .map_err(|_| anyhow!("Invalid URI"))?, +// ) +// .header(headers::CSEQ, rtsp_session.cseq.to_string()) +// .header(headers::USER_AGENT, USER_AGENT) +// .header( +// headers::SESSION, +// rtsp_session.session_id.as_ref().unwrap().as_str(), +// ) +// .empty(); + +// rtsp_session +// .send_request(&play_request.map_body(|_| vec![])) +// .await?; +// let mut play_response = rtsp_session.read_response().await?; +// trace!("play_response: {:?}", play_response); + +// if play_response.status() == StatusCode::Unauthorized { +// if let Some(auth_header) = play_response.header(&WWW_AUTHENTICATE).cloned() { +// play_response = rtsp_session +// .handle_unauthorized(Method::Play, &auth_header) +// .await?; +// } +// } + +// if play_response.status() != StatusCode::Ok { +// return Err(anyhow!("PLAY request failed")); +// } + +// tokio::spawn(rtsp_session.keep_rtsp_alive()); + +// let video_codec = video_track +// .and_then(|md| { +// md.attributes.iter().find_map(|attr| { +// if attr.attribute == "rtpmap" { +// let parts: Vec<&str> = attr.value.as_ref()?.split_whitespace().collect(); +// if parts.len() > 1 { +// Some(parts[1].split('/').next().unwrap_or("").to_string()) +// } else { +// None +// } +// } else { +// None +// } +// }) +// }) +// .and_then(|codec_str| codec_from_str(&codec_str).ok()); + +// let audio_codec = audio_track +// .and_then(|md| { +// md.attributes.iter().find_map(|attr| { +// if attr.attribute == "rtpmap" { +// let parts: Vec<&str> = attr.value.as_ref()?.split_whitespace().collect(); +// if parts.len() > 1 { +// Some(parts[1].split('/').next().unwrap_or("").to_string()) +// } else { +// None +// } +// } else { +// None +// } +// }) +// }) +// .and_then(|codec_str| codec_from_str(&codec_str).ok()); + +// let media_info = rtsp::MediaInfo { +// video_rtp_client: Some(video_port), +// video_rtcp_client: Some(video_port + 1), +// video_rtp_server: None, +// audio_rtp_client: Some(audio_port), +// audio_rtp_server: None, +// video_codec, +// audio_codec, +// }; + +// Ok(media_info) +// } +pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Codec)> { let mut url = Url::parse(rtsp_url)?; let host = url .host() @@ -326,8 +502,8 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result { return Err(anyhow!("No tracks found in SDP")); } - let video_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; - let audio_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; + let rtp_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; + let rtp_audio_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; let video_uri = video_track .and_then(|md| { @@ -367,7 +543,7 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result { trace!("audio uri: {:?}", audio_uri); rtsp_session.uri.clone_from(&video_uri); - rtsp_session.rtp_client_port = Some(video_port); + rtsp_session.rtp_client_port = Some(rtp_port); let session_id = rtsp_session.send_setup_request().await?; trace!("session id: {:?}", session_id); @@ -375,7 +551,7 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result { rtsp_session.session_id = Some(session_id); rtsp_session.uri.clone_from(&audio_uri); - rtsp_session.rtp_client_port = Some(audio_port); + rtsp_session.rtp_client_port = Some(rtp_audio_port); rtsp_session.send_setup_request().await?; let play_request = Request::builder(Method::Play, Version::V1_0) @@ -428,7 +604,7 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result { } }) }) - .and_then(|codec_str| codec_from_str(&codec_str).ok()); + .unwrap_or_else(|| "unknown".to_string()); let audio_codec = audio_track .and_then(|md| { @@ -445,17 +621,10 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result { } }) }) - .and_then(|codec_str| codec_from_str(&codec_str).ok()); - - let media_info = rtsp::MediaInfo { - video_rtp_client: Some(video_port), - video_rtcp_client: Some(video_port + 1), - video_rtp_server: None, - audio_rtp_client: Some(audio_port), - audio_rtp_server: None, - video_codec, - audio_codec, - }; + .unwrap_or_else(|| "unknown".to_string()); + + let video_codec = codec_from_str(&video_codec)?; + let audio_codec = codec_from_str(&audio_codec)?; - Ok(media_info) + Ok((rtp_port, rtp_audio_port, video_codec, audio_codec)) } From 1e2430ae6680d242abc8906da848352f66625947 Mon Sep 17 00:00:00 2001 From: Marsyew Date: Sun, 29 Sep 2024 20:13:40 +0800 Subject: [PATCH 04/15] fix cargo fmt --- tools/whipinto/src/rtspclient.rs | 176 ------------------------------- 1 file changed, 176 deletions(-) diff --git a/tools/whipinto/src/rtspclient.rs b/tools/whipinto/src/rtspclient.rs index 08c9966a..aed02a60 100644 --- a/tools/whipinto/src/rtspclient.rs +++ b/tools/whipinto/src/rtspclient.rs @@ -283,182 +283,6 @@ impl RtspSession { } } -// pub async fn setup_rtsp_session(rtsp_url: &str) -> Result { -// let mut url = Url::parse(rtsp_url)?; -// let host = url -// .host() -// .ok_or_else(|| anyhow!("Host not found"))? -// .to_string(); -// let port = url.port().unwrap_or(DEFAULT_RTSP_PORT); - -// let addr = format!("{}:{}", host, port); -// info!("Connecting to RTSP server at {}", addr); -// let stream = TcpStream::connect(addr).await?; - -// let mut rtsp_session = RtspSession { -// stream, -// uri: url.as_str().to_string(), -// cseq: 1, -// auth_params: AuthParams { -// username: url.username().to_string(), -// password: url.password().unwrap_or("").to_string(), -// }, -// session_id: None, -// rtp_client_port: None, -// auth_header: None, -// }; - -// url.set_username("").unwrap(); -// url.set_password(None).unwrap(); - -// rtsp_session.send_options_request().await?; - -// let sdp_content = rtsp_session.send_describe_request().await?; - -// let sdp: Session = Session::parse(sdp_content.as_bytes()) -// .map_err(|e| anyhow!("Failed to parse SDP: {}", e))?; - -// let video_track = sdp.medias.iter().find(|md| md.media == "video"); -// let audio_track = sdp.medias.iter().find(|md| md.media == "audio"); -// debug!("track video: {:?}, audio: {:?}", video_track, audio_track); - -// if video_track.is_none() && audio_track.is_none() { -// return Err(anyhow!("No tracks found in SDP")); -// } - -// let video_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; -// let audio_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; - -// let video_uri = video_track -// .and_then(|md| { -// md.attributes.iter().find_map(|attr| { -// if attr.attribute == "control" { -// let value = attr.value.clone().unwrap_or_default(); -// if value.starts_with("rtsp://") { -// Some(value) -// } else { -// Some(format!("{}/{}", rtsp_session.uri, value)) -// } -// } else { -// None -// } -// }) -// }) -// .unwrap_or_else(|| format!("{}/trackID=1", rtsp_session.uri)); - -// let audio_uri = audio_track -// .and_then(|md| { -// md.attributes.iter().find_map(|attr| { -// if attr.attribute == "control" { -// let value = attr.value.clone().unwrap_or_default(); -// if value.starts_with("rtsp://") { -// Some(value) -// } else { -// Some(format!("{}/{}", rtsp_session.uri, value)) -// } -// } else { -// None -// } -// }) -// }) -// .unwrap_or_else(|| format!("{}/trackID=2", rtsp_session.uri)); - -// trace!("video uri: {:?}", video_uri); -// trace!("audio uri: {:?}", audio_uri); - -// rtsp_session.uri.clone_from(&video_uri); -// rtsp_session.rtp_client_port = Some(video_port); - -// let session_id = rtsp_session.send_setup_request().await?; -// trace!("session id: {:?}", session_id); - -// rtsp_session.session_id = Some(session_id); - -// rtsp_session.uri.clone_from(&audio_uri); -// rtsp_session.rtp_client_port = Some(audio_port); -// rtsp_session.send_setup_request().await?; - -// let play_request = Request::builder(Method::Play, Version::V1_0) -// .request_uri( -// rtsp_session -// .uri -// .parse::() -// .map_err(|_| anyhow!("Invalid URI"))?, -// ) -// .header(headers::CSEQ, rtsp_session.cseq.to_string()) -// .header(headers::USER_AGENT, USER_AGENT) -// .header( -// headers::SESSION, -// rtsp_session.session_id.as_ref().unwrap().as_str(), -// ) -// .empty(); - -// rtsp_session -// .send_request(&play_request.map_body(|_| vec![])) -// .await?; -// let mut play_response = rtsp_session.read_response().await?; -// trace!("play_response: {:?}", play_response); - -// if play_response.status() == StatusCode::Unauthorized { -// if let Some(auth_header) = play_response.header(&WWW_AUTHENTICATE).cloned() { -// play_response = rtsp_session -// .handle_unauthorized(Method::Play, &auth_header) -// .await?; -// } -// } - -// if play_response.status() != StatusCode::Ok { -// return Err(anyhow!("PLAY request failed")); -// } - -// tokio::spawn(rtsp_session.keep_rtsp_alive()); - -// let video_codec = video_track -// .and_then(|md| { -// md.attributes.iter().find_map(|attr| { -// if attr.attribute == "rtpmap" { -// let parts: Vec<&str> = attr.value.as_ref()?.split_whitespace().collect(); -// if parts.len() > 1 { -// Some(parts[1].split('/').next().unwrap_or("").to_string()) -// } else { -// None -// } -// } else { -// None -// } -// }) -// }) -// .and_then(|codec_str| codec_from_str(&codec_str).ok()); - -// let audio_codec = audio_track -// .and_then(|md| { -// md.attributes.iter().find_map(|attr| { -// if attr.attribute == "rtpmap" { -// let parts: Vec<&str> = attr.value.as_ref()?.split_whitespace().collect(); -// if parts.len() > 1 { -// Some(parts[1].split('/').next().unwrap_or("").to_string()) -// } else { -// None -// } -// } else { -// None -// } -// }) -// }) -// .and_then(|codec_str| codec_from_str(&codec_str).ok()); - -// let media_info = rtsp::MediaInfo { -// video_rtp_client: Some(video_port), -// video_rtcp_client: Some(video_port + 1), -// video_rtp_server: None, -// audio_rtp_client: Some(audio_port), -// audio_rtp_server: None, -// video_codec, -// audio_codec, -// }; - -// Ok(media_info) -// } pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Codec)> { let mut url = Url::parse(rtsp_url)?; let host = url From 60d6345b8db0eb47c5227ea92f8dfc8e9eb82649 Mon Sep 17 00:00:00 2001 From: Marsyew Date: Sun, 29 Sep 2024 20:26:15 +0800 Subject: [PATCH 05/15] Remove redundant test file --- tools/whipinto/test.sdp | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 tools/whipinto/test.sdp diff --git a/tools/whipinto/test.sdp b/tools/whipinto/test.sdp deleted file mode 100644 index 1ad0b418..00000000 --- a/tools/whipinto/test.sdp +++ /dev/null @@ -1,13 +0,0 @@ -v=0 -o=- 0 0 IN IP4 127.0.0.1 -s=No Name -t=0 0 -a=tool:libavformat 61.3.100 -m=video 8004 RTP/AVP 96 -c=IN IP4 192.168.31.243 -b=AS:500 -a=rtpmap:96 VP8/90000 -m=audio 8006 RTP/AVP 97 -c=IN IP4 192.168.31.243 -b=AS:64 -a=rtpmap:97 G722/8000/1 \ No newline at end of file From 05628431899be03866c32ebc9344be6fbb0ffd8b Mon Sep 17 00:00:00 2001 From: Marsyew Date: Mon, 30 Sep 2024 17:15:09 +0800 Subject: [PATCH 06/15] fix whipinto single video/audio error --- tools/whepfrom/output.sdp | 8 ++ tools/whepfrom/src/main.rs | 29 +++-- tools/whipinto/output.sdp | 13 ++ tools/whipinto/src/main.rs | 251 ++++++++++++++++++------------------- 4 files changed, 157 insertions(+), 144 deletions(-) create mode 100644 tools/whepfrom/output.sdp create mode 100644 tools/whipinto/output.sdp diff --git a/tools/whepfrom/output.sdp b/tools/whepfrom/output.sdp new file mode 100644 index 00000000..053a0ea7 --- /dev/null +++ b/tools/whepfrom/output.sdp @@ -0,0 +1,8 @@ +v=0 +o=- 8421014732604292639 566074500 IN IP4 0.0.0.0 +s=- +t=0 0 +m=video 21767 RTP/AVP 96 +c=IN IP4 0.0.0.0 +a=rtpmap:96 VP8/90000 +a=control:streamid=0 diff --git a/tools/whepfrom/src/main.rs b/tools/whepfrom/src/main.rs index f262e5a3..ac474d1b 100644 --- a/tools/whepfrom/src/main.rs +++ b/tools/whepfrom/src/main.rs @@ -87,7 +87,7 @@ async fn main() -> Result<()> { )) .unwrap(), ); - info!("=== Received Input: {} ===", args.output); + info!("=== Received Output: {} ===", args.output); let mut host = match input.host().unwrap() { Host::Domain(_) | Host::Ipv4(_) => Ipv4Addr::UNSPECIFIED.to_string(), @@ -118,11 +118,10 @@ async fn main() -> Result<()> { codec_info.clone(), ) .await?; - info!("answer sdp: {:?}", answer.sdp); tokio::time::sleep(Duration::from_secs(1)).await; let codec_info = codec_info.lock().await; - debug!("code info {:?}", codec_info); + debug!("Codec Info {:?}", codec_info); let filtered_sdp = match rtsp::filter_sdp( &answer.sdp, @@ -132,7 +131,7 @@ async fn main() -> Result<()> { Ok(sdp) => sdp, Err(e) => e, }; - info!("Filtered SDP: {:?}", filtered_sdp); + info!("SDP: {:?}", filtered_sdp); if input.scheme() == SCHEME_RTSP_SERVER { let (tx, mut rx) = unbounded_channel::(); @@ -160,8 +159,6 @@ async fn main() -> Result<()> { }); media_info = rx.recv().await.unwrap(); - - println!("Media info: {:?}", media_info); } else { media_info.video_rtp_client = pick_unused_port(); media_info.audio_rtp_client = pick_unused_port(); @@ -188,16 +185,9 @@ async fn main() -> Result<()> { let sdp = session.marshal(); let mut file = File::create("output.sdp")?; file.write_all(sdp.as_bytes())?; + info!("SDP written to output.sdp"); } - - let child = Arc::new(create_child(args.command)?); - defer!({ - if let Some(child) = child.as_ref() { - if let Ok(mut child) = child.lock() { - let _ = child.kill(); - } - } - }); + debug!("media info : {:?}", media_info); tokio::spawn(rtp_send( video_recv, host.clone(), @@ -211,6 +201,15 @@ async fn main() -> Result<()> { media_info.audio_rtp_server, )); + let child = Arc::new(create_child(args.command)?); + defer!({ + if let Some(child) = child.as_ref() { + if let Ok(mut child) = child.lock() { + let _ = child.kill(); + } + } + }); + let wait_child = child.clone(); tokio::spawn(async move { match wait_child.as_ref() { diff --git a/tools/whipinto/output.sdp b/tools/whipinto/output.sdp new file mode 100644 index 00000000..aa1594e8 --- /dev/null +++ b/tools/whipinto/output.sdp @@ -0,0 +1,13 @@ +v=0 +o=- 0 0 IN IP4 127.0.0.1 +s=No Name +t=0 0 +a=tool:libavformat 61.3.100 +m=audio 11111 RTP/AVP 97 +c=IN IP4 192.168.129.114 +b=AS:64 +a=rtpmap:97 opus/48000/2 +m=video 11113 RTP/AVP 96 +c=IN IP4 192.168.129.114 +b=AS:256 +a=rtpmap:96 VP8/90000 diff --git a/tools/whipinto/src/main.rs b/tools/whipinto/src/main.rs index 8e95558f..cc66a6fb 100644 --- a/tools/whipinto/src/main.rs +++ b/tools/whipinto/src/main.rs @@ -30,7 +30,6 @@ use webrtc::{ use cli::{codec_from_str, create_child}; use libwish::Client; -use tokio::sync::mpsc::UnboundedReceiver; use rtspclient::setup_rtsp_session; @@ -106,8 +105,26 @@ async fn main() -> Result<()> { } let video_port = input.port().unwrap_or(0); + let media_info; let (complete_tx, mut complete_rx) = unbounded_channel(); + let mut client = Client::new( + args.whip.clone(), + Client::get_auth_header_map(args.auth_basic.clone(), args.auth_token.clone()), + ); + + let child = if let Some(command) = &args.command { + Arc::new(create_child(Some(command.to_string()))?) + } else { + Default::default() + }; + defer!({ + if let Some(child) = child.as_ref() { + if let Ok(mut child) = child.lock() { + let _ = child.kill(); + } + } + }); if input.scheme() == SCHEME_RTSP_SERVER { let (tx, mut rx) = unbounded_channel::(); @@ -132,20 +149,11 @@ async fn main() -> Result<()> { } }); - while let Some(media_info) = rx.recv().await { - let _ = setup_webrtc_session( - &args, - &complete_tx, - &mut complete_rx, - &media_info, - host.clone(), - ) - .await; - } + media_info = rx.recv().await.unwrap(); } else if input.scheme() == SCHEME_RTSP_CLIENT { let (rtp_port, audio_port, video_codec, audio_codec) = setup_rtsp_session(&args.input).await?; - let media_info = rtsp::MediaInfo { + media_info = rtsp::MediaInfo { video_rtp_client: None, audio_rtp_client: None, video_codec: Some(video_codec), @@ -154,16 +162,17 @@ async fn main() -> Result<()> { audio_rtp_server: Some(audio_port), video_rtcp_client: Some(rtp_port + 1), }; - let _ = - setup_webrtc_session(&args, &complete_tx, &mut complete_rx, &media_info, host).await; } else { + tokio::time::sleep(Duration::from_secs(1)).await; let sdp = sdp_types::Session::parse(&fs::read(&args.input).unwrap()).unwrap(); let video_track = sdp.medias.iter().find(|md| md.media == "video"); let audio_track = sdp.medias.iter().find(|md| md.media == "audio"); - let codec_vid = video_track - .and_then(|md| { - md.attributes.iter().find_map(|attr| { + let codec_vid = if let Some(video_track) = video_track { + video_track + .attributes + .iter() + .find_map(|attr| { if attr.attribute == "rtpmap" { let parts: Vec<&str> = attr.value.as_ref()?.split_whitespace().collect(); if parts.len() > 1 { @@ -175,11 +184,16 @@ async fn main() -> Result<()> { None } }) - }) - .unwrap_or_else(|| "unknown".to_string()); - let codec_aud = audio_track - .and_then(|md| { - md.attributes.iter().find_map(|attr| { + .unwrap_or_else(|| "unknown".to_string()) + } else { + "unknown".to_string() + }; + + let codec_aud = if let Some(audio_track) = audio_track { + audio_track + .attributes + .iter() + .find_map(|attr| { if attr.attribute == "rtpmap" { let parts: Vec<&str> = attr.value.as_ref()?.split_whitespace().collect(); if parts.len() > 1 { @@ -191,21 +205,98 @@ async fn main() -> Result<()> { None } }) - }) - .unwrap_or_else(|| "unknown".to_string()); - let media_info = rtsp::MediaInfo { + .unwrap_or_else(|| "unknown".to_string()) + } else { + "unknown".to_string() + }; + + media_info = rtsp::MediaInfo { video_rtp_client: None, audio_rtp_client: None, - video_codec: Some(codec_from_str(&codec_vid)?), - audio_codec: Some(codec_from_str(&codec_aud)?), + video_codec: if codec_vid != "unknown" { + Some(codec_from_str(&codec_vid)?) + } else { + None + }, + audio_codec: if codec_aud != "unknown" { + Some(codec_from_str(&codec_aud)?) + } else { + None + }, video_rtp_server: video_track.map(|track| track.port), audio_rtp_server: audio_track.map(|track| track.port), video_rtcp_client: None, }; + } + debug!("media info: {:?}", media_info); + let mut video_listener = None; + if let Some(video_port) = media_info.video_rtp_server { + video_listener = Some(UdpSocket::bind(format!("{}:{}", host, video_port)).await?); + } + let mut audio_listener = None; + if let Some(audio_port) = media_info.audio_rtp_server { + audio_listener = Some(UdpSocket::bind(format!("{}:{}", host, audio_port)).await?); + } + + let (peer, video_sender, audio_sender) = webrtc_start( + &mut client, + media_info.video_codec.map(|c| c.into()), + media_info.audio_codec.map(|c| c.into()), + complete_tx.clone(), + &args, + ) + .await + .map_err(|error| anyhow!(format!("[{}] {}", PREFIX_LIB, error)))?; + + if let Some(video_listener) = video_listener { + info!( + "=== video listener started : {} ===", + video_listener.local_addr().unwrap() + ); + tokio::spawn(rtp_listener(video_listener, video_sender)); + } + if let Some(audio_listener) = audio_listener { + info!( + "=== audio listener started : {} ===", + audio_listener.local_addr().unwrap() + ); + tokio::spawn(rtp_listener(audio_listener, audio_sender)); + } + + if let Some(port) = media_info.video_rtp_server { + tokio::spawn(rtcp_listener(host.clone(), port + 1, peer.clone())); + } + + let senders = peer.get_senders().await; + if let Some(rtcp_sender_port) = media_info.video_rtcp_client { + for sender in senders { + tokio::spawn(read_rtcp(sender, host.clone(), rtcp_sender_port)); + } + } + + let wait_child = child.clone(); + match wait_child.as_ref() { + Some(child) => loop { + if let Ok(mut child) = child.lock() { + if let Ok(wait) = child.try_wait() { + if wait.is_some() { + let _ = complete_tx.send(()); + return Ok(()); + } + } + } + tokio::time::sleep(Duration::from_secs(1)).await; + }, + None => info!("No child process"), + } - let _ = - setup_webrtc_session(&args, &complete_tx, &mut complete_rx, &media_info, host).await; + tokio::select! { + _ = complete_rx.recv() => {} + msg = signal::wait_for_stop_signal() => {warn!("Received signal: {}", msg)} } + warn!("RTP listener closed"); + let _ = client.remove_resource().await; + let _ = peer.close().await; Ok(()) } @@ -237,7 +328,7 @@ async fn rtcp_listener(host: String, rtcp_port: u16, peer: Arc, host: String, port: u16) -> Result } Err(err) => { warn!("Error reading RTCP packet from remote peer: {}", err); + break Ok(()); } } } } -async fn setup_webrtc_session( - args: &Args, - complete_tx: &UnboundedSender<()>, - complete_rx: &mut UnboundedReceiver<()>, - media_info: &rtsp::MediaInfo, - host: String, -) -> Result<()> { - let mut client = Client::new( - args.whip.clone(), - Client::get_auth_header_map(args.auth_basic.clone(), args.auth_token.clone()), - ); - - let child = if let Some(command) = &args.command { - let command = command.replace("{port}", "0"); - Arc::new(create_child(Some(command))?) - } else { - Default::default() - }; - - defer!({ - if let Some(child) = child.as_ref() { - if let Ok(mut child) = child.lock() { - let _ = child.kill(); - } - } - }); - - let (peer, video_sender, audio_sender) = webrtc_start( - &mut client, - media_info.video_codec.map(|c| c.into()), - media_info.audio_codec.map(|c| c.into()), - complete_tx.clone(), - args, - ) - .await - .map_err(|error| anyhow!(format!("[{}] {}", PREFIX_LIB, error)))?; - - if let Some(video_port) = media_info.video_rtp_server { - let video_listener = UdpSocket::bind(format!("{}:{}", host, video_port)).await?; - info!( - "=== video listener started : {} ===", - video_listener.local_addr().unwrap() - ); - tokio::spawn(rtp_listener(video_listener, video_sender)); - } else { - info!("Video stream is None"); - } - - if let Some(audio_port) = media_info.audio_rtp_server { - let audio_listener = UdpSocket::bind(format!("{}:{}", host, audio_port)).await?; - info!( - "=== audio listener started : {} ===", - audio_listener.local_addr().unwrap() - ); - tokio::spawn(rtp_listener(audio_listener, audio_sender)); - } else { - info!("Audio stream is None."); - } - - let rtcp_listener_port = media_info.video_rtp_server.unwrap_or(0) + 1; - tokio::spawn(rtcp_listener( - host.clone(), - rtcp_listener_port, - peer.clone(), - )); - let senders = peer.get_senders().await; - if let Some(rtcp_sender_port) = media_info.video_rtcp_client { - for sender in senders { - tokio::spawn(read_rtcp(sender, host.clone(), rtcp_sender_port)); - } - } - - let wait_child = child.clone(); - match wait_child.as_ref() { - Some(child) => loop { - if let Ok(mut child) = child.lock() { - if let Ok(wait) = child.try_wait() { - if wait.is_some() { - let _ = complete_tx.send(()); - return Ok(()); - } - } - } - tokio::time::sleep(Duration::from_secs(1)).await; - }, - None => info!("No child process"), - } - - tokio::select! { - _ = complete_rx.recv() => {} - msg = signal::wait_for_stop_signal() => {warn!("Received signal: {}", msg)} - } - warn!("RTP listener closed"); - let _ = client.remove_resource().await; - let _ = peer.close().await; - - Ok(()) -} - async fn webrtc_start( client: &mut Client, video_codec: Option, From 8b933e8ae2b3974b735b1e9a3808a01bf7264814 Mon Sep 17 00:00:00 2001 From: Marsyew Date: Mon, 30 Sep 2024 17:15:52 +0800 Subject: [PATCH 07/15] delete useless files --- tools/whepfrom/output.sdp | 8 -------- tools/whipinto/output.sdp | 13 ------------- 2 files changed, 21 deletions(-) delete mode 100644 tools/whepfrom/output.sdp delete mode 100644 tools/whipinto/output.sdp diff --git a/tools/whepfrom/output.sdp b/tools/whepfrom/output.sdp deleted file mode 100644 index 053a0ea7..00000000 --- a/tools/whepfrom/output.sdp +++ /dev/null @@ -1,8 +0,0 @@ -v=0 -o=- 8421014732604292639 566074500 IN IP4 0.0.0.0 -s=- -t=0 0 -m=video 21767 RTP/AVP 96 -c=IN IP4 0.0.0.0 -a=rtpmap:96 VP8/90000 -a=control:streamid=0 diff --git a/tools/whipinto/output.sdp b/tools/whipinto/output.sdp deleted file mode 100644 index aa1594e8..00000000 --- a/tools/whipinto/output.sdp +++ /dev/null @@ -1,13 +0,0 @@ -v=0 -o=- 0 0 IN IP4 127.0.0.1 -s=No Name -t=0 0 -a=tool:libavformat 61.3.100 -m=audio 11111 RTP/AVP 97 -c=IN IP4 192.168.129.114 -b=AS:64 -a=rtpmap:97 opus/48000/2 -m=video 11113 RTP/AVP 96 -c=IN IP4 192.168.129.114 -b=AS:256 -a=rtpmap:96 VP8/90000 From 071c61234f8855e81919d7bd8045fb5c32dec22a Mon Sep 17 00:00:00 2001 From: Marsyew Date: Tue, 1 Oct 2024 20:54:46 +0800 Subject: [PATCH 08/15] add whipinto/whepfrom aduio rtcp --- Cargo.lock | 1 + libs/rtsp/Cargo.toml | 1 + libs/rtsp/src/lib.rs | 17 +++++++++-------- tools/whepfrom/src/main.rs | 12 ++++++++++-- tools/whipinto/src/main.rs | 32 +++++++++++++++++++++++++------- 5 files changed, 46 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 389e1540..481a909d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2087,6 +2087,7 @@ dependencies = [ "sdp 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "sdp-types", "tokio", + "tracing", "webrtc", ] diff --git a/libs/rtsp/Cargo.toml b/libs/rtsp/Cargo.toml index e68b2572..ed2e164e 100644 --- a/libs/rtsp/Cargo.toml +++ b/libs/rtsp/Cargo.toml @@ -8,6 +8,7 @@ crate-type = ["lib"] [dependencies] anyhow = { workspace = true } +tracing = { workspace = true } rtsp-types = "0.1.1" sdp = "0.6" diff --git a/libs/rtsp/src/lib.rs b/libs/rtsp/src/lib.rs index 8158b219..df23653c 100644 --- a/libs/rtsp/src/lib.rs +++ b/libs/rtsp/src/lib.rs @@ -11,6 +11,7 @@ use tokio::{ net::TcpStream, sync::mpsc::UnboundedSender, }; +use tracing::{debug, error, warn}; use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecParameters; const SERVER_NAME: &str = "whipinto"; @@ -28,6 +29,7 @@ pub struct MediaInfo { pub video_rtcp_client: Option, pub video_rtp_server: Option, pub audio_rtp_client: Option, + pub audio_rtcp_client: Option, pub audio_rtp_server: Option, pub video_codec: Option, pub audio_codec: Option, @@ -79,6 +81,7 @@ impl Handler { video_rtcp_client: self.media_info.video_rtcp_client, video_rtp_server: self.media_info.video_rtp_server, audio_rtp_client: self.media_info.audio_rtp_client, + audio_rtcp_client: self.media_info.audio_rtcp_client, audio_rtp_server: self.media_info.audio_rtp_server, video_codec: self.media_info.video_codec, audio_codec: self.media_info.audio_codec, @@ -98,23 +101,22 @@ impl Handler { video_rtcp_client: self.media_info.video_rtcp_client, video_rtp_server: self.media_info.video_rtp_server, audio_rtp_client: self.media_info.audio_rtp_client, + audio_rtcp_client: self.media_info.audio_rtcp_client, audio_rtp_server: self.media_info.audio_rtp_server, video_codec: self.media_info.video_codec, audio_codec: self.media_info.audio_codec, }) .unwrap(); - println!("recordrrrrr {:?}", self.media_info); - Response::builder(req.version(), StatusCode::Ok) .header(headers::CSEQ, req.header(&headers::CSEQ).unwrap().as_str()) .header(headers::SERVER, "whipinto") .build(self.sdp.clone().unwrap()) } - fn describe(&mut self, req: &Request>) -> Response> { + fn describe(&self, req: &Request>) -> Response> { if self.sdp.is_none() { - println!("sdp is none"); + error!("SDP data is none"); } Response::builder(req.version(), StatusCode::Ok) @@ -163,6 +165,7 @@ impl Handler { let audio_rtcp_server_port = audio_server_port + 1; self.media_info.audio_rtp_client = Some(rtp); + self.media_info.audio_rtcp_client = rtcp; self.media_info.audio_rtp_server = Some(audio_server_port); self.media_info.audio_codec = media .attributes @@ -246,9 +249,8 @@ impl Handler { .build(Vec::new()); } } - println!("Updated self.media_info: {:?}", self.media_info); } else { - println!("SDP data is not available"); + warn!("SDP data is not available"); } } @@ -261,7 +263,7 @@ impl Handler { fn announce(&mut self, req: &Request>) -> Response> { self.set_sdp(req.body().to_vec()); let sdp = Session::parse(req.body()).unwrap(); - println!("parsed sdp: {:?}", sdp); + debug!("parsed sdp: {:?}", sdp); Response::builder(req.version(), StatusCode::Ok) .header(headers::CSEQ, req.header(&headers::CSEQ).unwrap().as_str()) @@ -338,7 +340,6 @@ pub async fn process_socket(mut socket: TcpStream, handler: &mut Handler) -> Res continue; } Err(e) => { - println!("parse error: {:?}", e); return Err(anyhow!("parse error: {:?}", e)); } } diff --git a/tools/whepfrom/src/main.rs b/tools/whepfrom/src/main.rs index ac474d1b..3134ebd3 100644 --- a/tools/whepfrom/src/main.rs +++ b/tools/whepfrom/src/main.rs @@ -46,20 +46,23 @@ const SCHEME_RTP_SDP: &str = "sdp"; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { + /// Verbose mode [default: "warn", -v "info", -vv "debug", -vvv "trace"] #[arg(short = 'v', action = ArgAction::Count, default_value_t = 0)] verbose: u8, #[arg(short, long, default_value_t = format!("{}://0.0.0.0:8555", SCHEME_RTSP_SERVER))] output: String, #[arg(long)] host: Option, - /// The WHIP server endpoint to POST SDP offer to. e.g.: https://example.com/whip/777 + /// The WHEP server endpoint to POST SDP offer to. e.g.: https://example.com/whep/777 #[arg(short, long)] whep: String, /// Run a command as childprocess #[arg(long)] command: Option, + /// Authentication basic to use, will be sent in the HTTP Header as 'Basic ' e.g.: admin:public #[arg(long)] auth_basic: Option, + /// Authentication token to use, will be sent in the HTTP Header as 'Bearer ' #[arg(long)] auth_token: Option, } @@ -233,6 +236,11 @@ async fn main() -> Result<()> { media_info.video_rtp_server, peer.clone(), )); + tokio::spawn(rtcp_listener( + host.clone(), + media_info.audio_rtp_server, + peer.clone(), + )); } tokio::select! { @@ -273,7 +281,7 @@ async fn rtp_send( while let Some(data) = receiver.recv().await { match socket.send_to(&data, &client_addr).await { - Ok(_) => debug!("Data sent to {}", client_addr), + Ok(_) => {} Err(e) => error!("Failed to send data to {}: {}", client_addr, e), } } diff --git a/tools/whipinto/src/main.rs b/tools/whipinto/src/main.rs index cc66a6fb..52a7933f 100644 --- a/tools/whipinto/src/main.rs +++ b/tools/whipinto/src/main.rs @@ -21,7 +21,9 @@ use webrtc::{ }, rtcp, rtp::packet::Packet, - rtp_transceiver::{rtp_codec::RTCRtpCodecCapability, rtp_sender::RTCRtpSender}, + rtp_transceiver::{ + rtp_codec::RTCRtpCodecCapability, rtp_codec::RTPCodecType, rtp_sender::RTCRtpSender, + }, track::track_local::{ track_local_static_rtp::TrackLocalStaticRTP, TrackLocal, TrackLocalWriter, }, @@ -151,16 +153,17 @@ async fn main() -> Result<()> { media_info = rx.recv().await.unwrap(); } else if input.scheme() == SCHEME_RTSP_CLIENT { - let (rtp_port, audio_port, video_codec, audio_codec) = + let (video_port, audio_port, video_codec, audio_codec) = setup_rtsp_session(&args.input).await?; media_info = rtsp::MediaInfo { video_rtp_client: None, audio_rtp_client: None, video_codec: Some(video_codec), audio_codec: Some(audio_codec), - video_rtp_server: Some(rtp_port), + video_rtp_server: Some(video_port), audio_rtp_server: Some(audio_port), - video_rtcp_client: Some(rtp_port + 1), + video_rtcp_client: Some(video_port + 1), + audio_rtcp_client: Some(audio_port + 1), }; } else { tokio::time::sleep(Duration::from_secs(1)).await; @@ -226,6 +229,7 @@ async fn main() -> Result<()> { video_rtp_server: video_track.map(|track| track.port), audio_rtp_server: audio_track.map(|track| track.port), video_rtcp_client: None, + audio_rtcp_client: None, }; } debug!("media info: {:?}", media_info); @@ -268,9 +272,23 @@ async fn main() -> Result<()> { } let senders = peer.get_senders().await; - if let Some(rtcp_sender_port) = media_info.video_rtcp_client { - for sender in senders { - tokio::spawn(read_rtcp(sender, host.clone(), rtcp_sender_port)); + if let Some(video_rtcp_port) = media_info.video_rtcp_client { + for sender in &senders { + if let Some(track) = sender.track().await { + if track.kind() == RTPCodecType::Video { + tokio::spawn(read_rtcp(sender.clone(), host.clone(), video_rtcp_port)); + } + } + } + } + + if let Some(audio_rtcp_port) = media_info.audio_rtcp_client { + for sender in &senders { + if let Some(track) = sender.track().await { + if track.kind() == RTPCodecType::Audio { + tokio::spawn(read_rtcp(sender.clone(), host.clone(), audio_rtcp_port)); + } + } } } From fec399564431a9bebf996410974fa15e38da8df1 Mon Sep 17 00:00:00 2001 From: Marsyew Date: Mon, 28 Oct 2024 21:57:44 +0800 Subject: [PATCH 09/15] fix:whipinto and whepfrom mode sdp file path failed at windows --- libs/cli/src/lib.rs | 8 ++++++++ livetwo/src/whep.rs | 31 ++++++++++++++++++++++--------- livetwo/src/whip.rs | 20 +++++++++++++------- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/libs/cli/src/lib.rs b/libs/cli/src/lib.rs index 60887296..eb2e41bc 100644 --- a/libs/cli/src/lib.rs +++ b/libs/cli/src/lib.rs @@ -133,7 +133,14 @@ pub fn get_codec_type(codec: &RTCRtpCodecCapability) -> RTPCodecType { pub fn create_child(command: Option) -> Result>> { let child = if let Some(command) = command { + let command = if cfg!(windows) { + command.replace('\\', "/") + } else { + command + }; + let mut args = shellwords::split(&command)?; + Some(Mutex::new( Command::new(args.remove(0)) .args(args) @@ -145,5 +152,6 @@ pub fn create_child(command: Option) -> Result>> { } else { None }; + Ok(child) } diff --git a/livetwo/src/whep.rs b/livetwo/src/whep.rs index b1c28b76..7287e266 100644 --- a/livetwo/src/whep.rs +++ b/livetwo/src/whep.rs @@ -8,6 +8,7 @@ use sdp::{description::media::RangedPort, SessionDescription}; use std::{ fs::File, io::{Cursor, Write}, + path::Path, sync::Arc, time::Duration, }; @@ -58,12 +59,13 @@ pub async fn from( ); info!("=== Received Output: {} ===", target_url); - let mut host = match input - .host() - .unwrap_or_else(|| panic!("Invalid host for {}", input)) - { - Host::Domain(_) | Host::Ipv4(_) => Ipv4Addr::UNSPECIFIED.to_string(), - Host::Ipv6(_) => Ipv6Addr::UNSPECIFIED.to_string(), + let mut host = match input.host() { + Some(Host::Domain(_)) | Some(Host::Ipv4(_)) => Ipv4Addr::UNSPECIFIED.to_string(), + Some(Host::Ipv6(_)) => Ipv6Addr::UNSPECIFIED.to_string(), + None => { + eprintln!("Invalid host for {}, using default.", input); + Ipv4Addr::UNSPECIFIED.to_string() + } }; if let Some(ref h) = set_host { @@ -134,6 +136,12 @@ pub async fn from( let mut reader = Cursor::new(filtered_sdp.as_bytes()); let mut session = SessionDescription::unmarshal(&mut reader).unwrap(); + host = session + .clone() + .connection_information + .and_then(|conn_info| conn_info.address) + .map(|address| address.to_string()) + .unwrap_or("127.0.0.1".to_string()); for media in &mut session.media_descriptions { if media.media_name.media == "video" { if let Some(port) = media_info.video_rtp_client { @@ -152,9 +160,14 @@ pub async fn from( } } let sdp = session.marshal(); - let file_path = input.path().strip_prefix('/').unwrap(); - info!("SDP written to {}", file_path); - let mut file = File::create(file_path)?; + + let file_path = Path::new(&target_url); + info!("SDP written to {:?}", file_path); + let mut file = File::options() + .write(true) + .create(true) + .truncate(true) + .open(file_path)?; file.write_all(sdp.as_bytes())?; } debug!("media info : {:?}", media_info); diff --git a/livetwo/src/whip.rs b/livetwo/src/whip.rs index 1b216971..e2a84c2e 100644 --- a/livetwo/src/whip.rs +++ b/livetwo/src/whip.rs @@ -1,5 +1,6 @@ use std::fs; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::path::Path; use std::{sync::Arc, time::Duration, vec}; use anyhow::{anyhow, Result}; @@ -53,12 +54,13 @@ pub async fn into( ); info!("=== Received Input: {} ===", input); - let mut host = match input - .host() - .unwrap_or_else(|| panic!("Invalid host for {}", input)) - { - Host::Domain(_) | Host::Ipv4(_) => Ipv4Addr::UNSPECIFIED.to_string(), - Host::Ipv6(_) => Ipv6Addr::UNSPECIFIED.to_string(), + let mut host = match input.host() { + Some(Host::Domain(_)) | Some(Host::Ipv4(_)) => Ipv4Addr::UNSPECIFIED.to_string(), + Some(Host::Ipv6(_)) => Ipv6Addr::UNSPECIFIED.to_string(), + None => { + eprintln!("Invalid host for {}, using default.", input); + Ipv4Addr::UNSPECIFIED.to_string() + } }; if let Some(ref h) = set_host { @@ -124,7 +126,11 @@ pub async fn into( }; } else { tokio::time::sleep(Duration::from_secs(1)).await; - let sdp = sdp_types::Session::parse(&fs::read(&target_url).unwrap()).unwrap(); + let path = Path::new(&target_url); + let sdp = sdp_types::Session::parse(&fs::read(path).unwrap()).unwrap(); + if let Some(connection_info) = &sdp.connection { + host.clone_from(&connection_info.connection_address); + } let video_track = sdp.medias.iter().find(|md| md.media == "video"); let audio_track = sdp.medias.iter().find(|md| md.media == "audio"); From 6a536578cbe2fe268b42ec81cca43dee71dcc540 Mon Sep 17 00:00:00 2001 From: Marsyew Date: Tue, 29 Oct 2024 12:55:17 +0800 Subject: [PATCH 10/15] refactor:apply unit tests and error handling --- libs/cli/src/lib.rs | 7 +------ livetwo/src/whep.rs | 4 ++-- tests/tests.rs | 2 -- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/libs/cli/src/lib.rs b/libs/cli/src/lib.rs index eb2e41bc..4d8c9808 100644 --- a/libs/cli/src/lib.rs +++ b/libs/cli/src/lib.rs @@ -133,12 +133,7 @@ pub fn get_codec_type(codec: &RTCRtpCodecCapability) -> RTPCodecType { pub fn create_child(command: Option) -> Result>> { let child = if let Some(command) = command { - let command = if cfg!(windows) { - command.replace('\\', "/") - } else { - command - }; - + let command = command.replace('\\', "/"); let mut args = shellwords::split(&command)?; Some(Mutex::new( diff --git a/livetwo/src/whep.rs b/livetwo/src/whep.rs index 7287e266..90af382e 100644 --- a/livetwo/src/whep.rs +++ b/livetwo/src/whep.rs @@ -63,7 +63,7 @@ pub async fn from( Some(Host::Domain(_)) | Some(Host::Ipv4(_)) => Ipv4Addr::UNSPECIFIED.to_string(), Some(Host::Ipv6(_)) => Ipv6Addr::UNSPECIFIED.to_string(), None => { - eprintln!("Invalid host for {}, using default.", input); + error!("Invalid host for {}, using default.", input); Ipv4Addr::UNSPECIFIED.to_string() } }; @@ -141,7 +141,7 @@ pub async fn from( .connection_information .and_then(|conn_info| conn_info.address) .map(|address| address.to_string()) - .unwrap_or("127.0.0.1".to_string()); + .unwrap_or(Ipv4Addr::LOCALHOST.to_string()); for media in &mut session.media_descriptions { if media.media_name.media == "video" { if let Some(port) = media_info.video_rtp_client { diff --git a/tests/tests.rs b/tests/tests.rs index 880760b1..4c5ba800 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -81,7 +81,6 @@ async fn test_liveion_stream_create() { assert_eq!(1, body.len()); } -#[cfg(not(windows))] #[tokio::test] async fn test_liveion_stream_connect() { let cfg = liveion::config::Config::default(); @@ -208,7 +207,6 @@ a=rtpmap:96 VP8/90000 assert!(result.is_some()); } -#[cfg(not(windows))] #[tokio::test] async fn test_liveion_stream_ffmpeg() { let cfg = liveion::config::Config::default(); From 0215b33e45ee4283b12a969c7f8e52c48c409c49 Mon Sep 17 00:00:00 2001 From: a-wing <1@233.email> Date: Tue, 5 Nov 2024 16:52:31 +0800 Subject: [PATCH 11/15] refactor(libs/cli): create_child --- libs/cli/src/lib.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/libs/cli/src/lib.rs b/libs/cli/src/lib.rs index 4d8c9808..296c0032 100644 --- a/libs/cli/src/lib.rs +++ b/libs/cli/src/lib.rs @@ -132,21 +132,21 @@ pub fn get_codec_type(codec: &RTCRtpCodecCapability) -> RTPCodecType { } pub fn create_child(command: Option) -> Result>> { - let child = if let Some(command) = command { - let command = command.replace('\\', "/"); - let mut args = shellwords::split(&command)?; + Ok(match command { + Some(command) => { + #[cfg(windows)] + let command = command.replace('\\', "/"); - Some(Mutex::new( - Command::new(args.remove(0)) - .args(args) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn()?, - )) - } else { - None - }; - - Ok(child) + let mut args = shellwords::split(&command)?; + Some(Mutex::new( + Command::new(args.remove(0)) + .args(args) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()?, + )) + } + None => None, + }) } From 1b14bcdc3f6f426a4d1741f7b8d11e6834a561a1 Mon Sep 17 00:00:00 2001 From: Marsyew Date: Sun, 5 Jan 2025 22:08:57 +0800 Subject: [PATCH 12/15] add whepfrom rtsp client --- Cargo.lock | 14 +- input.sdp | 0 livetwo/Cargo.toml | 2 +- livetwo/src/rtspclient.rs | 322 +++++++++++++++++++++++++++++--------- livetwo/src/whep.rs | 8 +- livetwo/src/whip.rs | 13 +- output.sdp | 12 ++ whep.sdp | 8 + 8 files changed, 289 insertions(+), 90 deletions(-) create mode 100644 input.sdp create mode 100644 output.sdp create mode 100644 whep.sdp diff --git a/Cargo.lock b/Cargo.lock index 0da08dbc..1175e7b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1761,7 +1761,7 @@ dependencies = [ "rtsp", "rtsp-types", "scopeguard", - "sdp 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "sdp 0.7.0", "sdp-types", "signal", "tokio", @@ -3066,6 +3066,18 @@ dependencies = [ "url", ] +[[package]] +name = "sdp" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a526161f474ae94b966ba622379d939a8fe46c930eebbadb73e339622599d5" +dependencies = [ + "rand", + "substring", + "thiserror", + "url", +] + [[package]] name = "sdp-types" version = "0.1.6" diff --git a/input.sdp b/input.sdp new file mode 100644 index 00000000..e69de29b diff --git a/livetwo/Cargo.toml b/livetwo/Cargo.toml index 17dc3eee..d166fc0e 100644 --- a/livetwo/Cargo.toml +++ b/livetwo/Cargo.toml @@ -27,4 +27,4 @@ url = "2.5.2" portpicker = "0.1.1" rtsp-types = "0.1.2" sdp-types = "0.1" -sdp = "0.6.2" +sdp = "0.7.0" diff --git a/livetwo/src/rtspclient.rs b/livetwo/src/rtspclient.rs index 6c820ea7..ecfe83b7 100644 --- a/livetwo/src/rtspclient.rs +++ b/livetwo/src/rtspclient.rs @@ -164,6 +164,42 @@ impl RtspSession { Ok(()) } + async fn send_announce_request(&mut self, sdp: String) -> Result<()> { + let announce_request = Request::builder(Method::Announce, Version::V1_0) + .request_uri( + self.uri + .parse::() + .map_err(|_| anyhow!("Invalid URI"))?, + ) + .header(headers::CSEQ, self.cseq.to_string()) + .header(headers::CONTENT_TYPE, "application/sdp") + .header(headers::USER_AGENT, USER_AGENT) + .build(sdp.into_bytes()); + + self.send_request(&announce_request).await?; + let announce_response = self.read_response().await?; + self.cseq += 1; + + if announce_response.status() == StatusCode::Unauthorized { + if let Some(auth_header) = announce_response.header(&WWW_AUTHENTICATE).cloned() { + let announce_response = self + .handle_unauthorized(Method::Announce, &auth_header) + .await?; + if announce_response.status() != StatusCode::Ok { + return Err(anyhow!("ANNOUNCE request failed after authentication")); + } + } else { + return Err(anyhow!( + "ANNOUNCE request failed with 401 Unauthorized and no WWW-Authenticate header" + )); + } + } else if announce_response.status() != StatusCode::Ok { + return Err(anyhow!("ANNOUNCE request failed")); + } + + Ok(()) + } + async fn send_describe_request(&mut self) -> Result { let describe_request = Request::builder(Method::Describe, Version::V1_0) .request_uri( @@ -197,7 +233,7 @@ impl RtspSession { Ok(sdp_content) } - async fn send_setup_request(&mut self) -> Result { + async fn send_setup_request(&mut self) -> Result<(String, u16)> { let rtp_client_port = self .rtp_client_port .ok_or_else(|| anyhow!("RTP server port not set"))?; @@ -267,11 +303,26 @@ impl RtspSession { .ok_or_else(|| anyhow!("Failed to parse session ID"))? .to_string(); - Ok(session_id) + let transport_header = setup_response + .header(&headers::TRANSPORT) + .ok_or_else(|| anyhow!("Transport header not found"))? + .as_str(); + + let server_port = transport_header + .split(';') + .find_map(|part| part.strip_prefix("server_port=")) + .and_then(|server_port_str| server_port_str.split('-').next()) + .ok_or_else(|| anyhow!("server_port not found in transport header"))? + .parse::() + .map_err(|_| anyhow!("Failed to parse server port"))?; + + info!("Transport header: {}", transport_header); + + Ok((session_id, server_port)) } } -pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Codec)> { +pub async fn setup_rtsp_session(rtsp_url: &str) -> Result { let mut url = Url::parse(rtsp_url)?; let host = url .host() @@ -314,12 +365,114 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Code return Err(anyhow!("No tracks found in SDP")); } - let rtp_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; - let rtp_audio_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; + let mut media_info = rtsp::MediaInfo::default(); + + if let Some(video_track) = video_track { + let (rtp_client, rtp_server, codec) = + setup_track(&mut rtsp_session, video_track, "0").await?; + media_info.video_rtp_server = rtp_client; + media_info.video_rtp_client = rtp_server; + media_info.video_codec = codec; + } + + if let Some(audio_track) = audio_track { + let (rtp_client, rtp_server, codec) = + setup_track(&mut rtsp_session, audio_track, "1").await?; + media_info.audio_rtp_server = rtp_client; + media_info.audio_rtp_client = rtp_server; + media_info.audio_codec = codec; + } + + let play_request = Request::builder(Method::Play, Version::V1_0) + .request_uri( + rtsp_session + .uri + .parse::() + .map_err(|_| anyhow!("Invalid URI"))?, + ) + .header(headers::CSEQ, rtsp_session.cseq.to_string()) + .header(headers::USER_AGENT, USER_AGENT) + .header( + headers::SESSION, + rtsp_session.session_id.as_ref().unwrap().as_str(), + ) + .empty(); + + rtsp_session + .send_request(&play_request.map_body(|_| vec![])) + .await?; + let mut play_response = rtsp_session.read_response().await?; + trace!("play_response: {:?}", play_response); + + if play_response.status() == StatusCode::Unauthorized { + if let Some(auth_header) = play_response.header(&WWW_AUTHENTICATE).cloned() { + play_response = rtsp_session + .handle_unauthorized(Method::Play, &auth_header) + .await?; + } + } + + if play_response.status() != StatusCode::Ok { + return Err(anyhow!("PLAY request failed")); + } + + tokio::spawn(rtsp_session.keep_rtsp_alive()); + + Ok(media_info) +} + +pub async fn setup_rtsp_push_session( + rtsp_url: &str, + sdp_content: String, +) -> Result { + let mut url = Url::parse(rtsp_url)?; + let host = url.host_str().ok_or_else(|| anyhow!("Invalid RTSP URL"))?; + let port = url + .port_or_known_default() + .ok_or_else(|| anyhow!("Invalid RTSP URL"))?; + + let addr = format!("{}:{}", host, port); + info!("Connecting to RTSP server at {}", addr); + let stream = TcpStream::connect(&addr).await?; + + let mut rtsp_session = RtspSession { + stream, + uri: url.as_str().to_string(), + cseq: 1, + auth_params: AuthParams { + username: url.username().to_string(), + password: url.password().unwrap_or("").to_string(), + }, + session_id: None, + rtp_client_port: None, + auth_header: None, + }; + + url.set_username("").unwrap(); + url.set_password(None).unwrap(); - let video_uri = video_track - .and_then(|md| { - md.attributes.iter().find_map(|attr| { + rtsp_session.send_options_request().await?; + rtsp_session + .send_announce_request(sdp_content.clone()) + .await?; + + let sdp: Session = Session::parse(sdp_content.as_bytes()) + .map_err(|e| anyhow!("Failed to parse SDP: {}", e))?; + + let video_track = sdp.medias.iter().find(|md| md.media == "video"); + let audio_track = sdp.medias.iter().find(|md| md.media == "audio"); + debug!("track video: {:?}, audio: {:?}", video_track, audio_track); + + if video_track.is_none() && audio_track.is_none() { + return Err(anyhow!("No tracks found in SDP")); + } + let mut media_info = rtsp::MediaInfo::default(); + + if let Some(video_track) = video_track { + let video_url = video_track + .attributes + .iter() + .find_map(|attr| { if attr.attribute == "control" { let value = attr.value.clone().unwrap_or_default(); if value.starts_with("rtsp://") { @@ -331,12 +484,23 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Code None } }) - }) - .unwrap_or_else(|| format!("{}/trackID=1", rtsp_session.uri)); + .unwrap_or_else(|| format!("{}/trackID=1", rtsp_session.uri)); + + media_info.video_rtp_client = + Some(pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?); + rtsp_session.rtp_client_port = media_info.video_rtp_client; + rtsp_session.uri = video_url; - let audio_uri = audio_track - .and_then(|md| { - md.attributes.iter().find_map(|attr| { + let (session_id, v_server_port) = rtsp_session.send_setup_request().await?; + rtsp_session.session_id = Some(session_id); + media_info.video_rtp_server = Some(v_server_port); + } + + if let Some(audio_track) = audio_track { + let audio_url = audio_track + .attributes + .iter() + .find_map(|attr| { if attr.attribute == "control" { let value = attr.value.clone().unwrap_or_default(); if value.starts_with("rtsp://") { @@ -348,25 +512,18 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Code None } }) - }) - .unwrap_or_else(|| format!("{}/trackID=2", rtsp_session.uri)); + .unwrap_or_else(|| format!("{}/trackID=2", rtsp_session.uri)); - trace!("video uri: {:?}", video_uri); - trace!("audio uri: {:?}", audio_uri); - - rtsp_session.uri.clone_from(&video_uri); - rtsp_session.rtp_client_port = Some(rtp_port); - - let session_id = rtsp_session.send_setup_request().await?; - trace!("session id: {:?}", session_id); - - rtsp_session.session_id = Some(session_id); + media_info.audio_rtp_client = + Some(pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?); + rtsp_session.rtp_client_port = media_info.audio_rtp_client; + rtsp_session.uri = audio_url; - rtsp_session.uri.clone_from(&audio_uri); - rtsp_session.rtp_client_port = Some(rtp_audio_port); - rtsp_session.send_setup_request().await?; + let (_session_id, a_server_port) = rtsp_session.send_setup_request().await?; + media_info.audio_rtp_server = Some(a_server_port); + } - let play_request = Request::builder(Method::Play, Version::V1_0) + let record_request = Request::builder(Method::Record, Version::V1_0) .request_uri( rtsp_session .uri @@ -377,68 +534,85 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result<(u16, u16, Codec, Code .header(headers::USER_AGENT, USER_AGENT) .header( headers::SESSION, - rtsp_session.session_id.as_ref().unwrap().as_str(), + rtsp_session + .session_id + .clone() + .ok_or_else(|| anyhow!("Missing session ID"))?, ) .empty(); rtsp_session - .send_request(&play_request.map_body(|_| vec![])) + .send_request(&record_request.map_body(|_| vec![])) .await?; - let mut play_response = rtsp_session.read_response().await?; - trace!("play_response: {:?}", play_response); + let response = rtsp_session.read_response().await?; + rtsp_session.cseq += 1; - if play_response.status() == StatusCode::Unauthorized { - if let Some(auth_header) = play_response.header(&WWW_AUTHENTICATE).cloned() { - play_response = rtsp_session - .handle_unauthorized(Method::Play, &auth_header) + if response.status() == StatusCode::Unauthorized { + if let Some(auth_header) = response.header(&WWW_AUTHENTICATE).cloned() { + let response = rtsp_session + .handle_unauthorized(Method::Record, &auth_header) .await?; + if response.status() != StatusCode::Ok { + return Err(anyhow!("RECORD request failed after authentication")); + } + } else { + return Err(anyhow!( + "RECORD request failed with 401 Unauthorized and no WWW-Authenticate header" + )); } - } - - if play_response.status() != StatusCode::Ok { - return Err(anyhow!("PLAY request failed")); + } else if response.status() != StatusCode::Ok { + return Err(anyhow!( + "RECORD request failed with status: {:?}", + response.status() + )); } tokio::spawn(rtsp_session.keep_rtsp_alive()); - let video_codec = video_track - .and_then(|md| { - md.attributes.iter().find_map(|attr| { - if attr.attribute == "rtpmap" { - let parts: Vec<&str> = attr.value.as_ref()?.split_whitespace().collect(); - if parts.len() > 1 { - Some(parts[1].split('/').next().unwrap_or("").to_string()) - } else { - None - } - } else { - None - } - }) - }) - .unwrap_or_else(|| "unknown".to_string()); - - let audio_codec = audio_track - .and_then(|md| { - md.attributes.iter().find_map(|attr| { - if attr.attribute == "rtpmap" { - let parts: Vec<&str> = attr.value.as_ref()?.split_whitespace().collect(); - if parts.len() > 1 { - Some(parts[1].split('/').next().unwrap_or("").to_string()) - } else { - None - } + Ok(media_info) +} + +async fn setup_track( + rtsp_session: &mut RtspSession, + track: &sdp_types::Media, + track_id: &str, +) -> Result<(Option, Option, Option)> { + let track_url = track + .attributes + .iter() + .find_map(|attr| { + if attr.attribute == "control" { + let value = attr.value.clone().unwrap_or_default(); + if value.starts_with("rtsp://") { + Some(value) } else { - None + Some(format!("{}/{}", rtsp_session.uri, value)) } - }) + } else { + None + } }) - .unwrap_or_else(|| "unknown".to_string()); + .unwrap_or_else(|| format!("{}/trackID={}", rtsp_session.uri, track_id)); + + let rtp_client_port = pick_unused_port().ok_or_else(|| anyhow!("No available port found"))?; + rtsp_session.rtp_client_port = Some(rtp_client_port); + rtsp_session.uri = track_url; - let video_codec = codec_from_str(&video_codec)?; - let audio_codec = codec_from_str(&audio_codec)?; + let (session_id, rtp_server_port) = rtsp_session.send_setup_request().await?; + rtsp_session.session_id = Some(session_id); + + let rtcp_client_port = Some(rtp_client_port + 1); + + let codec = track.attributes.iter().find_map(|attr| { + if attr.attribute == "rtpmap" { + let value = attr.value.as_ref()?.split_whitespace().nth(1)?; + codec_from_str(value).ok() + } else { + None + } + }); - Ok((rtp_port, rtp_audio_port, video_codec, audio_codec)) + Ok((Some(rtp_client_port), Some(rtp_server_port), codec)) } fn generate_digest_response( diff --git a/livetwo/src/whep.rs b/livetwo/src/whep.rs index 90af382e..e5470d3a 100644 --- a/livetwo/src/whep.rs +++ b/livetwo/src/whep.rs @@ -39,7 +39,8 @@ use webrtc::{ use libwish::Client; -use crate::{PREFIX_LIB, SCHEME_RTP_SDP, SCHEME_RTSP_SERVER}; +use crate::rtspclient::setup_rtsp_push_session; +use crate::{PREFIX_LIB, SCHEME_RTP_SDP, SCHEME_RTSP_CLIENT, SCHEME_RTSP_SERVER}; pub async fn from( target_url: String, @@ -107,7 +108,7 @@ pub async fn from( if input.scheme() == SCHEME_RTSP_SERVER { let (tx, mut rx) = unbounded_channel::(); let mut handler = rtsp::Handler::new(tx, complete_tx.clone()); - handler.set_sdp(filtered_sdp.into_bytes()); + handler.set_sdp(filtered_sdp.clone().into_bytes()); let host2 = host.to_string(); let tcp_port = input.port().unwrap_or(0); @@ -130,6 +131,9 @@ pub async fn from( }); media_info = rx.recv().await.unwrap(); + } + if input.scheme() == SCHEME_RTSP_CLIENT { + media_info = setup_rtsp_push_session(&target_url, filtered_sdp.clone()).await?; } else { media_info.video_rtp_client = pick_unused_port(); media_info.audio_rtp_client = pick_unused_port(); diff --git a/livetwo/src/whip.rs b/livetwo/src/whip.rs index e2a84c2e..2785b9de 100644 --- a/livetwo/src/whip.rs +++ b/livetwo/src/whip.rs @@ -112,18 +112,7 @@ pub async fn into( media_info = rx.recv().await.unwrap(); } else if input.scheme() == SCHEME_RTSP_CLIENT { - let (video_port, audio_port, video_codec, audio_codec) = - setup_rtsp_session(&target_url).await?; - media_info = rtsp::MediaInfo { - video_rtp_client: None, - audio_rtp_client: None, - video_codec: Some(video_codec), - audio_codec: Some(audio_codec), - video_rtp_server: Some(video_port), - audio_rtp_server: Some(audio_port), - video_rtcp_client: Some(video_port + 1), - audio_rtcp_client: Some(audio_port + 1), - }; + media_info = setup_rtsp_session(&target_url).await?; } else { tokio::time::sleep(Duration::from_secs(1)).await; let path = Path::new(&target_url); diff --git a/output.sdp b/output.sdp new file mode 100644 index 00000000..641c9873 --- /dev/null +++ b/output.sdp @@ -0,0 +1,12 @@ +v=0 +o=- 1251730756246557359 686736100 IN IP4 0.0.0.0 +s=- +t=0 0 +m=video 15527 RTP/AVP 96 +c=IN IP4 0.0.0.0 +a=rtpmap:96 VP8/90000 +a=control:streamid=0 +m=audio 22625 RTP/AVP 111 +c=IN IP4 0.0.0.0 +a=rtpmap:111 opus/48000/2 +a=control:streamid=1 diff --git a/whep.sdp b/whep.sdp new file mode 100644 index 00000000..e332ccc1 --- /dev/null +++ b/whep.sdp @@ -0,0 +1,8 @@ +v=0 +o=- 2203881470464365150 896677300 IN IP4 0.0.0.0 +s=- +t=0 0 +m=video 24874 RTP/AVP 96 +c=IN IP4 0.0.0.0 +a=rtpmap:96 VP8/90000 +a=control:streamid=0 From 3f4e0cdd97d3657eaba0f516514a255802390689 Mon Sep 17 00:00:00 2001 From: Marsyew Date: Sun, 5 Jan 2025 22:17:47 +0800 Subject: [PATCH 13/15] exclude SDP files --- .gitignore | Bin 353 -> 410 bytes input.sdp | 0 output.sdp | 12 ------------ whep.sdp | 8 -------- 4 files changed, 20 deletions(-) delete mode 100644 input.sdp delete mode 100644 output.sdp delete mode 100644 whep.sdp diff --git a/.gitignore b/.gitignore index 04bb13255d6fbde0c26aa0e2f432b6c6e9cd96c8..a530fe3a5ea3f187919e7a314dfe4f6f009a2b3d 100644 GIT binary patch literal 410 zcmZWkF>b>!4Ad+@e{hj4K;+S5mv-wGAZwsXj4MMH1yXjJ-p}WKig@8pI&~tCs=b-n_ ze?iMQbRRg1AZ_(%1&z*SJ_Yz(#?~H0?O`}M`AI+b;htHXe&jQ zYg#=qlhN&O@8syV)O&~(sSA<73yHv;%b~l)&1zTnlGV#b>!4BYt&7u^g*9zAwxw{8Kt2CT%m3S`nCX{Y&pr5FK71=lITfNw# Ma%uCz4X-co16!MS*#H0l diff --git a/input.sdp b/input.sdp deleted file mode 100644 index e69de29b..00000000 diff --git a/output.sdp b/output.sdp deleted file mode 100644 index 641c9873..00000000 --- a/output.sdp +++ /dev/null @@ -1,12 +0,0 @@ -v=0 -o=- 1251730756246557359 686736100 IN IP4 0.0.0.0 -s=- -t=0 0 -m=video 15527 RTP/AVP 96 -c=IN IP4 0.0.0.0 -a=rtpmap:96 VP8/90000 -a=control:streamid=0 -m=audio 22625 RTP/AVP 111 -c=IN IP4 0.0.0.0 -a=rtpmap:111 opus/48000/2 -a=control:streamid=1 diff --git a/whep.sdp b/whep.sdp deleted file mode 100644 index e332ccc1..00000000 --- a/whep.sdp +++ /dev/null @@ -1,8 +0,0 @@ -v=0 -o=- 2203881470464365150 896677300 IN IP4 0.0.0.0 -s=- -t=0 0 -m=video 24874 RTP/AVP 96 -c=IN IP4 0.0.0.0 -a=rtpmap:96 VP8/90000 -a=control:streamid=0 From ccb1b5cff6ba6816ea5be0fb9f280c6d468be464 Mon Sep 17 00:00:00 2001 From: Marsyew Date: Sun, 5 Jan 2025 22:41:36 +0800 Subject: [PATCH 14/15] fix clippy error --- livetwo/src/rtspclient.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/livetwo/src/rtspclient.rs b/livetwo/src/rtspclient.rs index ecfe83b7..7a9eceb0 100644 --- a/livetwo/src/rtspclient.rs +++ b/livetwo/src/rtspclient.rs @@ -368,17 +368,19 @@ pub async fn setup_rtsp_session(rtsp_url: &str) -> Result { let mut media_info = rtsp::MediaInfo::default(); if let Some(video_track) = video_track { - let (rtp_client, rtp_server, codec) = + let (rtp_client, rtcp_client, rtp_server, codec) = setup_track(&mut rtsp_session, video_track, "0").await?; media_info.video_rtp_server = rtp_client; + media_info.video_rtcp_client = rtcp_client; media_info.video_rtp_client = rtp_server; media_info.video_codec = codec; } if let Some(audio_track) = audio_track { - let (rtp_client, rtp_server, codec) = + let (rtp_client, rtcp_client, rtp_server, codec) = setup_track(&mut rtsp_session, audio_track, "1").await?; media_info.audio_rtp_server = rtp_client; + media_info.audio_rtcp_client = rtcp_client; media_info.audio_rtp_client = rtp_server; media_info.audio_codec = codec; } @@ -576,7 +578,7 @@ async fn setup_track( rtsp_session: &mut RtspSession, track: &sdp_types::Media, track_id: &str, -) -> Result<(Option, Option, Option)> { +) -> Result<(Option, Option, Option, Option)> { let track_url = track .attributes .iter() @@ -601,8 +603,6 @@ async fn setup_track( let (session_id, rtp_server_port) = rtsp_session.send_setup_request().await?; rtsp_session.session_id = Some(session_id); - let rtcp_client_port = Some(rtp_client_port + 1); - let codec = track.attributes.iter().find_map(|attr| { if attr.attribute == "rtpmap" { let value = attr.value.as_ref()?.split_whitespace().nth(1)?; @@ -612,7 +612,7 @@ async fn setup_track( } }); - Ok((Some(rtp_client_port), Some(rtp_server_port), codec)) + Ok((Some(rtp_client_port), Some(rtp_client_port + 1), Some(rtp_server_port), codec)) } fn generate_digest_response( From c625e8fa9d42c7685936edc9fa9510dc17dfa70d Mon Sep 17 00:00:00 2001 From: Marsyew Date: Sun, 5 Jan 2025 22:47:49 +0800 Subject: [PATCH 15/15] style: format statement --- livetwo/src/rtspclient.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/livetwo/src/rtspclient.rs b/livetwo/src/rtspclient.rs index 7a9eceb0..4c005d9f 100644 --- a/livetwo/src/rtspclient.rs +++ b/livetwo/src/rtspclient.rs @@ -612,7 +612,12 @@ async fn setup_track( } }); - Ok((Some(rtp_client_port), Some(rtp_client_port + 1), Some(rtp_server_port), codec)) + Ok(( + Some(rtp_client_port), + Some(rtp_client_port + 1), + Some(rtp_server_port), + codec, + )) } fn generate_digest_response(