use ff_format::{AudioCodec, VideoCodec};
use crate::error::StreamError;
use crate::rtmp_inner::RtmpInner;
pub struct RtmpOutput {
url: String,
video_width: Option<u32>,
video_height: Option<u32>,
fps: Option<f64>,
sample_rate: u32,
channels: u32,
video_codec: VideoCodec,
audio_codec: AudioCodec,
video_bitrate: u64,
audio_bitrate: u64,
inner: Option<RtmpInner>,
finished: bool,
}
impl RtmpOutput {
#[must_use]
pub fn new(url: &str) -> Self {
Self {
url: url.to_owned(),
video_width: None,
video_height: None,
fps: None,
sample_rate: 44100,
channels: 2,
video_codec: VideoCodec::H264,
audio_codec: AudioCodec::Aac,
video_bitrate: 4_000_000,
audio_bitrate: 128_000,
inner: None,
finished: false,
}
}
pub fn build(mut self) -> Result<Self, StreamError> {
if !self.url.starts_with("rtmp://") {
return Err(StreamError::InvalidConfig {
reason: "RtmpOutput URL must start with rtmp://".into(),
});
}
let (Some(width), Some(height), Some(fps)) =
(self.video_width, self.video_height, self.fps)
else {
return Err(StreamError::InvalidConfig {
reason: "video parameters not set; call .video(width, height, fps) before .build()"
.into(),
});
};
if self.video_codec != VideoCodec::H264 {
return Err(StreamError::UnsupportedCodec {
codec: format!("{:?}", self.video_codec),
reason: "RTMP/FLV requires H.264 video".into(),
});
}
if self.audio_codec != AudioCodec::Aac {
return Err(StreamError::UnsupportedCodec {
codec: format!("{:?}", self.audio_codec),
reason: "RTMP/FLV requires AAC audio".into(),
});
}
#[allow(clippy::cast_possible_truncation)]
let fps_int = fps.round().max(1.0) as i32;
let inner = RtmpInner::open(
&self.url,
width.cast_signed(),
height.cast_signed(),
fps_int,
self.video_bitrate,
self.sample_rate.cast_signed(),
self.channels.cast_signed(),
self.audio_bitrate.cast_signed(),
)?;
self.inner = Some(inner);
Ok(self)
}
}
impl_live_stream_setters!(RtmpOutput, required_audio);
impl_frame_push_stream_output!(RtmpOutput);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_without_rtmp_scheme_should_return_invalid_config() {
let result = RtmpOutput::new("http://example.com/live")
.video(1280, 720, 30.0)
.build();
assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
}
#[test]
fn build_without_video_should_return_invalid_config() {
let result = RtmpOutput::new("rtmp://localhost/live/key").build();
assert!(matches!(result, Err(StreamError::InvalidConfig { .. })));
}
#[test]
fn build_with_non_h264_video_codec_should_return_unsupported_codec() {
let result = RtmpOutput::new("rtmp://localhost/live/key")
.video(1280, 720, 30.0)
.video_codec(VideoCodec::Vp9)
.build();
assert!(matches!(result, Err(StreamError::UnsupportedCodec { .. })));
}
#[test]
fn build_with_non_aac_audio_codec_should_return_unsupported_codec() {
let result = RtmpOutput::new("rtmp://localhost/live/key")
.video(1280, 720, 30.0)
.audio_codec(AudioCodec::Mp3)
.build();
assert!(matches!(result, Err(StreamError::UnsupportedCodec { .. })));
}
#[test]
fn video_bitrate_default_should_be_four_megabits() {
let out = RtmpOutput::new("rtmp://localhost/live/key");
assert_eq!(out.video_bitrate, 4_000_000);
}
#[test]
fn audio_defaults_should_be_44100hz_stereo() {
let out = RtmpOutput::new("rtmp://localhost/live/key");
assert_eq!(out.sample_rate, 44100);
assert_eq!(out.channels, 2);
}
}