Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] RTMP output #675

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compositor_api/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub use component::WebView;

pub use register_input::Mp4Input;
pub use register_output::Mp4Output;
pub use register_output::RtmpOutput;
pub use register_output::RtpOutput;

pub use register_input::DeckLink;
Expand Down
64 changes: 64 additions & 0 deletions compositor_api/src/types/from_register_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use compositor_pipeline::pipeline::{
output::{
self,
mp4::{Mp4AudioTrack, Mp4OutputOptions, Mp4VideoTrack},
rtmp::{RtmpAudioTrack, RtmpSenderOptions, RtmpVideoTrack},
},
};

Expand Down Expand Up @@ -173,6 +174,69 @@ impl TryFrom<Mp4Output> for pipeline::RegisterOutputOptions<output::OutputOption
}
}

impl TryFrom<RtmpOutput> for pipeline::RegisterOutputOptions<output::OutputOptions> {
type Error = TypeError;

fn try_from(value: RtmpOutput) -> Result<Self, Self::Error> {
let RtmpOutput { url, video, audio } = value;

if video.is_none() && audio.is_none() {
return Err(TypeError::new(
"At least one of \"video\" and \"audio\" fields have to be specified.",
));
}

let rtmp_video = video.as_ref().map(|v| match v.encoder {
VideoEncoderOptions::FfmpegH264 { .. } => RtmpVideoTrack {
width: v.resolution.width as u32,
height: v.resolution.height as u32,
},
});
let rtmp_audio = audio.as_ref().map(|a| match &a.encoder {
Mp4AudioEncoderOptions::Aac { channels } => RtmpAudioTrack {
channels: channels.clone().into(),
},
});

let (video_encoder_options, output_video_options) = maybe_video_options(video)?;
let (audio_encoder_options, output_audio_options) = match audio {
Some(OutputMp4AudioOptions {
mixing_strategy,
send_eos_when,
encoder,
initial,
}) => {
let audio_encoder_options: AudioEncoderOptions = encoder.into();
let output_audio_options = pipeline::OutputAudioOptions {
initial: initial.try_into()?,
end_condition: send_eos_when.unwrap_or_default().try_into()?,
mixing_strategy: mixing_strategy.unwrap_or(MixingStrategy::SumClip).into(),
channels: audio_encoder_options.channels(),
};

(Some(audio_encoder_options), Some(output_audio_options))
}
None => (None, None),
};

let output_options = output::OutputOptions {
output_protocol: output::OutputProtocolOptions::Rtmp(RtmpSenderOptions {
url,
video: rtmp_video,
audio: rtmp_audio,
}),
video: video_encoder_options,
audio: audio_encoder_options,
};

Ok(Self {
output_options,
video: output_video_options,
audio: output_audio_options,
})
}
}

fn maybe_video_options(
options: Option<OutputVideoOptions>,
) -> Result<
Expand Down
10 changes: 10 additions & 0 deletions compositor_api/src/types/register_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ pub struct RtpOutput {
pub audio: Option<OutputRtpAudioOptions>,
}

#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RtmpOutput {
pub url: String,
/// Video stream configuration.
pub video: Option<OutputVideoOptions>,
/// Audio stream configuration.
pub audio: Option<OutputMp4AudioOptions>,
}

#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Mp4Output {
Expand Down
26 changes: 26 additions & 0 deletions compositor_pipeline/src/pipeline/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use compositor_render::{
};
use crossbeam_channel::{bounded, Receiver, Sender};
use mp4::{Mp4FileWriter, Mp4OutputOptions};
use rtmp::RtmpSenderOptions;

use crate::{audio_mixer::OutputSamples, error::RegisterOutputError, queue::PipelineEvent};

Expand All @@ -15,6 +16,7 @@ use super::{
};

pub mod mp4;
pub mod rtmp;
pub mod rtp;

/// Options to configure public outputs that can be constructed via REST API
Expand All @@ -28,6 +30,7 @@ pub struct OutputOptions {
#[derive(Debug, Clone)]
pub enum OutputProtocolOptions {
Rtp(RtpSenderOptions),
Rtmp(RtmpSenderOptions),
Mp4(Mp4OutputOptions),
}

Expand Down Expand Up @@ -64,6 +67,10 @@ pub enum Output {
sender: RtpSender,
encoder: Encoder,
},
Rtmp {
sender: rtmp::RmtpSender,
encoder: Encoder,
},
Mp4 {
writer: Mp4FileWriter,
encoder: Encoder,
Expand Down Expand Up @@ -108,6 +115,17 @@ impl OutputOptionsExt<Option<Port>> for OutputOptions {

Ok((Output::Rtp { sender, encoder }, Some(port)))
}
OutputProtocolOptions::Rtmp(rtmp_options) => {
let sender = rtmp::RmtpSender::new(
output_id,
rtmp_options.clone(),
packets,
ctx.output_sample_rate,
)
.map_err(|e| RegisterOutputError::OutputError(output_id.clone(), e))?;

Ok((Output::Rtmp { sender, encoder }, None))
}
OutputProtocolOptions::Mp4(mp4_opt) => {
let writer = Mp4FileWriter::new(output_id.clone(), mp4_opt.clone(), packets, ctx)
.map_err(|e| RegisterOutputError::OutputError(output_id.clone(), e))?;
Expand Down Expand Up @@ -174,6 +192,7 @@ impl Output {
pub fn frame_sender(&self) -> Option<&Sender<PipelineEvent<Frame>>> {
match &self {
Output::Rtp { encoder, .. } => encoder.frame_sender(),
Output::Rtmp { encoder, .. } => encoder.frame_sender(),
Output::Mp4 { encoder, .. } => encoder.frame_sender(),
Output::EncodedData { encoder } => encoder.frame_sender(),
Output::RawData { video, .. } => video.as_ref(),
Expand All @@ -183,6 +202,7 @@ impl Output {
pub fn samples_batch_sender(&self) -> Option<&Sender<PipelineEvent<OutputSamples>>> {
match &self {
Output::Rtp { encoder, .. } => encoder.samples_batch_sender(),
Output::Rtmp { encoder, .. } => encoder.samples_batch_sender(),
Output::Mp4 { encoder, .. } => encoder.samples_batch_sender(),
Output::EncodedData { encoder } => encoder.samples_batch_sender(),
Output::RawData { audio, .. } => audio.as_ref(),
Expand All @@ -192,6 +212,7 @@ impl Output {
pub fn resolution(&self) -> Option<Resolution> {
match &self {
Output::Rtp { encoder, .. } => encoder.video.as_ref().map(|v| v.resolution()),
Output::Rtmp { encoder, .. } => encoder.video.as_ref().map(|v| v.resolution()),
Output::Mp4 { encoder, .. } => encoder.video.as_ref().map(|v| v.resolution()),
Output::EncodedData { encoder } => encoder.video.as_ref().map(|v| v.resolution()),
Output::RawData { resolution, .. } => *resolution,
Expand All @@ -201,6 +222,7 @@ impl Output {
pub fn request_keyframe(&self, output_id: OutputId) -> Result<(), RequestKeyframeError> {
let encoder = match &self {
Output::Rtp { encoder, .. } => encoder,
Output::Rtmp { encoder, .. } => encoder,
Output::Mp4 { encoder, .. } => encoder,
Output::EncodedData { encoder } => encoder,
Output::RawData { .. } => return Err(RequestKeyframeError::RawOutput(output_id)),
Expand All @@ -221,6 +243,10 @@ impl Output {
.video
.as_ref()
.map(|_| OutputFrameFormat::PlanarYuv420Bytes),
Output::Rtmp { encoder, .. } => encoder
.video
.as_ref()
.map(|_| OutputFrameFormat::PlanarYuv420Bytes),
Output::EncodedData { encoder } => encoder
.video
.as_ref()
Expand Down
15 changes: 1 addition & 14 deletions compositor_pipeline/src/pipeline/output/mp4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,6 @@ pub struct Mp4AudioTrack {
pub channels: AudioChannels,
}

pub enum Mp4OutputVideoTrack {
H264 { width: u32, height: u32 },
}

pub struct Mp4WriterOptions {
pub output_path: PathBuf,
pub video: Option<Mp4OutputVideoTrack>,
}

pub struct Mp4FileWriter;

impl Mp4FileWriter {
Expand Down Expand Up @@ -116,18 +107,14 @@ fn init_ffmpeg_output(
.map(|v| {
const VIDEO_TIME_BASE: i32 = 90000;

let codec = match v.codec {
VideoCodec::H264 => ffmpeg::codec::Id::H264,
};

let mut stream = output_ctx
.add_stream(ffmpeg::codec::Id::H264)
.map_err(OutputInitError::FfmpegMp4Error)?;

stream.set_time_base(ffmpeg::Rational::new(1, VIDEO_TIME_BASE));

let codecpar = unsafe { &mut *(*stream.as_mut_ptr()).codecpar };
codecpar.codec_id = codec.into();
codecpar.codec_id = ffmpeg::codec::Id::H264.into();
codecpar.codec_type = ffmpeg::ffi::AVMediaType::AVMEDIA_TYPE_VIDEO;
codecpar.width = v.width as i32;
codecpar.height = v.height as i32;
Expand Down
Loading
Loading