use std::{ffi::CString, ptr, sync::Arc};
use crate::pp_log::{PpLog, pp_error, pp_info};
use ffmpeg_next::{self as ffmpeg, ffi};
use thiserror::Error as ThisError;
use crate::{
buffer::MediaBuffer,
control::ControlMsg,
element::{Element, ElementType, Sink, element_pp_log},
elements::RtspTransport,
error::Result,
};
#[derive(Debug, ThisError)]
pub enum RtspSinkError {
#[error("ffmpeg error: {0}")]
Ffmpeg(#[from] ffmpeg::Error),
#[error(
"RtspSink only remuxes compressed Packets, got a decoded {0}; \
connect an encoder or demuxer packet pad instead"
)]
UnsupportedBuffer(&'static str),
#[error("RTSP URL contains a NUL byte")]
InvalidUrl,
}
pub struct RtspSink {
pp_log: PpLog,
name: Arc<str>,
url: String,
output: ffmpeg::format::context::Output,
input_time_base: ffmpeg::Rational,
last_output_dts: Option<i64>,
last_output_pts: Option<i64>,
pts_offset: i64,
pending_seek: bool,
}
impl RtspSink {
pub fn open(
name: impl Into<String>,
url: impl Into<String>,
transport: RtspTransport,
params: ffmpeg::codec::Parameters,
time_base: ffmpeg::Rational,
) -> Result<Self> {
let url = url.into();
let mut output = alloc_output(&url)?;
{
let mut stream = output
.add_stream(ffmpeg::encoder::find(ffmpeg::codec::Id::None))
.map_err(RtspSinkError::from)?;
stream.set_parameters(params);
unsafe {
(*stream.parameters().as_mut_ptr()).codec_tag = 0;
}
stream.set_time_base(time_base);
}
let mut options = ffmpeg::Dictionary::new();
options.set("rtsp_transport", transport.as_ffmpeg_option());
output
.write_header_with(options)
.map_err(RtspSinkError::from)?;
let name: Arc<str> = name.into().into();
let pp_log = element_pp_log(ElementType::RtspSink, &name, None);
pp_info!(pp_log: &pp_log, "publishing: url={url}, transport={transport:?}");
Ok(Self {
pp_log,
name,
url,
output,
input_time_base: time_base,
last_output_dts: None,
last_output_pts: None,
pts_offset: 0,
pending_seek: false,
})
}
pub fn url(&self) -> &str {
&self.url
}
}
fn alloc_output(url: &str) -> Result<ffmpeg::format::context::Output> {
let c_url = CString::new(url).map_err(|_| RtspSinkError::InvalidUrl)?;
let c_format = CString::new("rtsp").expect("static format name contains no NUL");
unsafe {
let mut context: *mut ffi::AVFormatContext = ptr::null_mut();
let result = ffi::avformat_alloc_output_context2(
&mut context,
ptr::null_mut(),
c_format.as_ptr(),
c_url.as_ptr(),
);
if result < 0 {
return Err(RtspSinkError::Ffmpeg(ffmpeg::Error::from(result)).into());
}
Ok(ffmpeg::format::context::Output::wrap(context))
}
}
impl Element for RtspSink {
fn name(&self) -> Arc<str> {
self.name.clone()
}
fn element_type(&self) -> ElementType {
ElementType::RtspSink
}
fn pp_log(&self) -> &PpLog {
&self.pp_log
}
fn pp_log_mut(&mut self) -> &mut PpLog {
&mut self.pp_log
}
}
impl Sink for RtspSink {
fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
match buf {
MediaBuffer::Packet(packet) => {
let mut packet = (*packet).clone();
let output_time_base = self
.output
.stream(0)
.expect("stream 0 was added by RtspSink::open")
.time_base();
packet.rescale_ts(self.input_time_base, output_time_base);
if let Some(raw_pts) = packet.pts() {
if self.pending_seek {
self.pts_offset = match (self.last_output_dts, packet.dts()) {
(Some(last_dts), Some(raw_dts)) => last_dts + 1 - raw_dts,
_ => match self.last_output_pts {
Some(last_pts) => last_pts + 1 - raw_pts,
None => 0,
},
};
self.pending_seek = false;
}
let corrected_pts = raw_pts + self.pts_offset;
packet.set_pts(Some(corrected_pts));
if let Some(raw_dts) = packet.dts() {
let corrected_dts = raw_dts + self.pts_offset;
packet.set_dts(Some(corrected_dts));
self.last_output_dts = Some(corrected_dts);
}
self.last_output_pts = Some(corrected_pts);
}
packet.set_stream(0);
packet.set_position(-1);
packet
.write_interleaved(&mut self.output)
.map_err(RtspSinkError::from)
.map_err(Into::into)
.inspect_err(|error| pp_error!(self, "write_interleaved failed: {error}"))
}
MediaBuffer::Eos => self
.output
.write_trailer()
.map_err(RtspSinkError::from)
.map_err(Into::into)
.inspect_err(|error| pp_error!(self, "write_trailer failed: {error}")),
MediaBuffer::Video(_) => {
pp_error!(self, "unsupported buffer: Video");
Err(RtspSinkError::UnsupportedBuffer("Video").into())
}
MediaBuffer::Audio(_) => {
pp_error!(self, "unsupported buffer: Audio");
Err(RtspSinkError::UnsupportedBuffer("Audio").into())
}
}
}
fn control(&mut self, msg: ControlMsg) -> Result<()> {
match msg {
ControlMsg::Seek(_) => self.pending_seek = true,
ControlMsg::Pause | ControlMsg::Resume | ControlMsg::Stop => {}
}
Ok(())
}
}
impl Drop for RtspSink {
fn drop(&mut self) {
pp_info!(
self,
"dropped: closing publisher connection to {}",
self.url
);
}
}
#[cfg(test)]
mod tests {
use ffmpeg_next as ffmpeg;
use super::{RtspSink, RtspSinkError};
use crate::{elements::RtspTransport, error::Error};
#[test]
fn rejects_a_url_containing_a_nul_byte_before_connecting() {
let result = RtspSink::open(
"rtsp",
"rtsp://127.0.0.1:8554/stream\0invalid",
RtspTransport::Tcp,
ffmpeg::codec::Parameters::new(),
ffmpeg::Rational(1, 1_000),
);
assert!(matches!(
result,
Err(Error::RtspSinkError(RtspSinkError::InvalidUrl))
));
}
}