use anyhow::Result;
use clap::Parser;
use env_logger::Target;
use futures::FutureExt;
use rtc::interceptor::Registry;
use rtc::media::Sample;
use rtc::media::io::ivf_reader::IVFReader;
use rtc::media_stream::MediaStreamTrack;
use rtc::peer_connection::configuration::RTCConfigurationBuilder;
use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors;
use rtc::peer_connection::configuration::media_engine::{MIME_TYPE_VP8, MediaEngine};
use rtc::peer_connection::sdp::RTCSessionDescription;
use rtc::peer_connection::transport::RTCIceServer;
use rtc::rtp_transceiver::rtp_sender::{RTCRtpCodec, RtpCodecKind};
use rtc::rtp_transceiver::rtp_sender::{
RTCRtpCodecParameters, RTCRtpCodingParameters, RTCRtpEncodingParameters,
};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use std::{
fs,
fs::{File, OpenOptions},
io::{BufReader, Write},
str::FromStr,
};
use webrtc::media_stream::track_local::TrackLocal;
use webrtc::media_stream::track_local::static_sample::TrackLocalStaticSample;
use webrtc::peer_connection::{
PeerConnection, PeerConnectionBuilder, PeerConnectionEventHandler, RTCIceGatheringState,
RTCPeerConnectionState,
};
use webrtc::runtime::{Sender, block_on, channel, default_runtime, interval};
const CIPHER_KEY: u8 = 0xAA;
#[derive(Parser)]
#[command(name = "insertable-streams")]
#[command(author = "Rain Liu <yliu@webrtc.rs>")]
#[command(version = "0.1.0")]
#[command(about = "An example of insertable-streams: XOR-encrypts each VP8 frame before sending.")]
struct Cli {
#[arg(short, long)]
debug: bool,
#[arg(short, long, default_value_t = format!("INFO"))]
log_level: String,
#[arg(short, long, default_value_t = format!(""))]
input_sdp_file: String,
#[arg(short, long, default_value_t = format!(""))]
output_log_file: String,
#[arg(short, long)]
video: String,
}
#[derive(Clone)]
struct Handler {
gather_complete_tx: Sender<()>,
done_tx: Sender<()>,
connected_tx: Sender<()>,
}
#[async_trait::async_trait]
impl PeerConnectionEventHandler for Handler {
async fn on_ice_gathering_state_change(&self, state: RTCIceGatheringState) {
if state == RTCIceGatheringState::Complete {
let _ = self.gather_complete_tx.try_send(());
}
}
async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
println!("Peer Connection State has changed: {state}");
match state {
RTCPeerConnectionState::Connected => {
let _ = self.connected_tx.try_send(());
}
RTCPeerConnectionState::Failed => {
let _ = self.done_tx.try_send(());
}
_ => {}
}
}
}
fn main() -> Result<()> {
block_on(async_main())
}
async fn async_main() -> Result<()> {
let cli = Cli::parse();
let log_level = log::LevelFilter::from_str(&cli.log_level)?;
if cli.debug {
env_logger::Builder::new()
.target(if !cli.output_log_file.is_empty() {
Target::Pipe(Box::new(
OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&cli.output_log_file)?,
))
} else {
Target::Stdout
})
.format(|buf, record| {
writeln!(
buf,
"{}:{} [{}] {} - {}",
record.file().unwrap_or("unknown"),
record.line().unwrap_or(0),
record.level(),
chrono::Local::now().format("%H:%M:%S.%6f"),
record.args()
)
})
.filter(None, log_level)
.init();
}
if !Path::new(&cli.video).exists() {
return Err(anyhow::anyhow!("video file: '{}' not exist", cli.video));
}
let mut media_engine = MediaEngine::default();
let video_codec = RTCRtpCodecParameters {
rtp_codec: RTCRtpCodec {
mime_type: MIME_TYPE_VP8.to_owned(),
clock_rate: 90000,
channels: 0,
sdp_fmtp_line: "".to_owned(),
rtcp_feedback: vec![],
},
payload_type: 96,
..Default::default()
};
media_engine.register_codec(video_codec.clone(), RtpCodecKind::Video)?;
let registry = register_default_interceptors(Registry::new(), &mut media_engine)?;
let config = RTCConfigurationBuilder::new()
.with_ice_servers(vec![RTCIceServer {
urls: vec!["stun:stun.l.google.com:19302".to_string()],
..Default::default()
}])
.build();
let (done_tx, mut done_rx) = channel::<()>(1);
let (gather_complete_tx, mut gather_complete_rx) = channel::<()>(1);
let (connected_tx, mut connected_rx) = channel::<()>(1);
let (ctrlc_tx, mut ctrlc_rx) = channel::<()>(1);
ctrlc::set_handler(move || {
let _ = ctrlc_tx.try_send(());
})?;
let runtime =
default_runtime().ok_or_else(|| std::io::Error::other("no async runtime found"))?;
let handler = Arc::new(Handler {
gather_complete_tx,
done_tx: done_tx.clone(),
connected_tx,
});
let peer_connection = PeerConnectionBuilder::new()
.with_configuration(config)
.with_media_engine(media_engine)
.with_interceptor_registry(registry)
.with_handler(handler)
.with_runtime(runtime.clone())
.with_udp_addrs(vec![format!("{}:0", signal::get_local_ip())])
.build()
.await?;
let ssrc = rand::random::<u32>();
let video_track = Arc::new(TrackLocalStaticSample::new(MediaStreamTrack::new(
format!("webrtc-rs-stream-id-{}", RtpCodecKind::Video),
format!("webrtc-rs-track-id-{}", RtpCodecKind::Video),
format!("webrtc-rs-track-label-{}", RtpCodecKind::Video),
RtpCodecKind::Video,
vec![RTCRtpEncodingParameters {
rtp_coding_parameters: RTCRtpCodingParameters {
ssrc: Some(ssrc),
..Default::default()
},
codec: video_codec.rtp_codec.clone(),
..Default::default()
}],
))?);
peer_connection
.add_track(Arc::clone(&video_track) as Arc<dyn TrackLocal>)
.await?;
print!("Paste offer from browser and press Enter: ");
let line = if cli.input_sdp_file.is_empty() {
signal::must_read_stdin()?
} else {
fs::read_to_string(&cli.input_sdp_file)?
};
let desc_data = signal::decode(line.as_str())?;
let offer = serde_json::from_str::<RTCSessionDescription>(&desc_data)?;
peer_connection.set_remote_description(offer).await?;
let answer = peer_connection.create_answer(None).await?;
peer_connection.set_local_description(answer).await?;
let _ = gather_complete_rx.recv().await;
if let Some(local_desc) = peer_connection.local_description().await {
let json_str = serde_json::to_string(&local_desc)?;
let b64 = signal::encode(&json_str);
println!("{b64}");
} else {
println!("generate local_description failed!");
}
println!("Waiting for peer connection...");
connected_rx.recv().await;
println!("Connected! Starting video stream.");
let video_file_name = cli.video.clone();
let (video_done_tx, mut video_done_rx) = channel::<()>(1);
runtime.spawn(Box::pin(async move {
if let Err(e) = stream_video(video_file_name, video_track, ssrc).await {
eprintln!("video streaming error: {e}");
}
let _ = video_done_tx.try_send(());
}));
println!("Press ctrl-c to stop");
futures::select! {
_ = done_rx.recv().fuse() => {
println!("received done signal!");
}
_ = ctrlc_rx.recv().fuse() => {
println!("received ctrl-c signal!");
}
_ = video_done_rx.recv().fuse() => {
println!("video streaming completed.");
}
}
peer_connection.close().await?;
Ok(())
}
async fn stream_video(
video_file_name: String,
track: Arc<TrackLocalStaticSample>,
ssrc: u32,
) -> Result<()> {
println!("play video from disk file {video_file_name}");
let file = File::open(&video_file_name)?;
let (mut ivf, header) = IVFReader::new(BufReader::new(file))?;
let frame_duration = Duration::from_millis(
((1000 * header.timebase_numerator) / header.timebase_denominator) as u64,
);
let mut ticker = interval(frame_duration);
loop {
let mut frame = match ivf.parse_next_frame() {
Ok((frame, _)) => frame,
Err(err) => {
println!("All video frames parsed and sent: {err}");
break;
}
};
for b in &mut frame[..] {
*b ^= CIPHER_KEY;
}
track
.sample_writer(ssrc)
.write_sample(&Sample {
data: frame.freeze(),
duration: frame_duration,
..Default::default()
})
.await?;
let _ = ticker.tick().await;
}
Ok(())
}