mirror of
https://github.com/v0l/zap-stream-core.git
synced 2025-06-20 05:30:29 +00:00
Configurable encoder pipeline
This commit is contained in:
112
src/decode/mod.rs
Normal file
112
src/decode/mod.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ptr;
|
||||
|
||||
use anyhow::Error;
|
||||
use ffmpeg_sys_next::{
|
||||
av_frame_alloc, av_packet_unref, avcodec_alloc_context3, avcodec_find_decoder,
|
||||
avcodec_free_context, avcodec_open2, avcodec_parameters_to_context, avcodec_receive_frame,
|
||||
avcodec_send_packet, AVCodec, AVCodecContext, AVPacket, AVStream, AVERROR, AVERROR_EOF,
|
||||
};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc::{Receiver, UnboundedReceiver};
|
||||
|
||||
use crate::pipeline::PipelinePayload;
|
||||
|
||||
struct CodecContext {
|
||||
pub context: *mut AVCodecContext,
|
||||
pub codec: *const AVCodec,
|
||||
}
|
||||
|
||||
impl Drop for CodecContext {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
avcodec_free_context(&mut self.context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Decoder {
|
||||
chan_in: UnboundedReceiver<PipelinePayload>,
|
||||
chan_out: broadcast::Sender<PipelinePayload>,
|
||||
codecs: HashMap<i32, CodecContext>,
|
||||
}
|
||||
|
||||
unsafe impl Send for Decoder {}
|
||||
unsafe impl Sync for Decoder {}
|
||||
|
||||
impl Decoder {
|
||||
pub fn new(
|
||||
chan_in: UnboundedReceiver<PipelinePayload>,
|
||||
chan_out: broadcast::Sender<PipelinePayload>,
|
||||
) -> Self {
|
||||
Self {
|
||||
chan_in,
|
||||
chan_out,
|
||||
codecs: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn decode_pkt(&mut self, pkt: *mut AVPacket) -> Result<(), Error> {
|
||||
let stream_index = (*pkt).stream_index as i32;
|
||||
let stream = (*pkt).opaque as *mut AVStream;
|
||||
let codec_par = (*stream).codecpar;
|
||||
let has_codec_params = codec_par != ptr::null_mut();
|
||||
if !has_codec_params {
|
||||
panic!("Cant handle pkt, dropped!");
|
||||
}
|
||||
|
||||
if has_codec_params && !self.codecs.contains_key(&stream_index) {
|
||||
let codec = avcodec_find_decoder((*codec_par).codec_id);
|
||||
if codec == ptr::null_mut() {
|
||||
return Err(Error::msg("Failed to find codec"));
|
||||
}
|
||||
let mut context = avcodec_alloc_context3(ptr::null());
|
||||
if context == ptr::null_mut() {
|
||||
return Err(Error::msg("Failed to alloc context"));
|
||||
}
|
||||
if avcodec_parameters_to_context(context, codec_par) != 0 {
|
||||
return Err(Error::msg("Failed to copy codec parameters to context"));
|
||||
}
|
||||
if avcodec_open2(context, codec, ptr::null_mut()) < 0 {
|
||||
return Err(Error::msg("Failed to open codec"));
|
||||
}
|
||||
self.codecs
|
||||
.insert(stream_index, CodecContext { context, codec });
|
||||
}
|
||||
if let Some(ctx) = self.codecs.get_mut(&stream_index) {
|
||||
let mut ret = -1;
|
||||
ret = avcodec_send_packet(ctx.context, pkt);
|
||||
av_packet_unref(pkt);
|
||||
if ret < 0 {
|
||||
return Err(Error::msg(format!("Failed to decode packet {}", ret)));
|
||||
}
|
||||
|
||||
while ret >= 0 {
|
||||
let frame = av_frame_alloc();
|
||||
ret = avcodec_receive_frame(ctx.context, frame);
|
||||
if ret < 0 {
|
||||
if ret == AVERROR_EOF || ret == AVERROR(libc::EAGAIN) {
|
||||
break;
|
||||
}
|
||||
return Err(Error::msg(format!("Failed to decode {}", ret)));
|
||||
}
|
||||
(*frame).time_base = (*pkt).time_base;
|
||||
self.chan_out.send(PipelinePayload::AvFrame(frame))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process(&mut self) -> Result<(), Error> {
|
||||
while let Ok(pkg) = self.chan_in.try_recv() {
|
||||
if let PipelinePayload::AvPacket(pkt) = pkg {
|
||||
unsafe {
|
||||
self.decode_pkt(pkt)?;
|
||||
}
|
||||
} else {
|
||||
return Err(Error::msg("Payload not supported"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
37
src/demux/info.rs
Normal file
37
src/demux/info.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use ffmpeg_sys_next::AVCodecParameters;
|
||||
use crate::fraction::Fraction;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DemuxStreamInfo {
|
||||
pub channels: Vec<StreamInfoChannel>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum StreamChannelType {
|
||||
Video,
|
||||
Audio,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct StreamInfoChannel {
|
||||
pub index: usize,
|
||||
pub channel_type: StreamChannelType,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub codec_params: *const AVCodecParameters,
|
||||
}
|
||||
|
||||
unsafe impl Sync for StreamInfoChannel {}
|
||||
unsafe impl Send for StreamInfoChannel {}
|
||||
|
||||
impl TryInto<Fraction> for StreamInfoChannel {
|
||||
type Error = ();
|
||||
|
||||
fn try_into(self) -> Result<Fraction, Self::Error> {
|
||||
if self.channel_type == StreamChannelType::Video {
|
||||
Ok(Fraction::from((self.width, self.height)))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
230
src/demux/mod.rs
230
src/demux/mod.rs
@ -1,14 +1,19 @@
|
||||
use std::ffi::CStr;
|
||||
use std::pin::Pin;
|
||||
use std::ptr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::Error;
|
||||
use async_trait::async_trait;
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use ffmpeg_sys_next::AVMediaType::{AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO};
|
||||
use ffmpeg_sys_next::*;
|
||||
use log::{debug, info, warn};
|
||||
use log::info;
|
||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::pipeline::{PipelinePayload, PipelineStep};
|
||||
use crate::demux::info::{DemuxStreamInfo, StreamChannelType, StreamInfoChannel};
|
||||
use crate::pipeline::PipelinePayload;
|
||||
use crate::utils::get_ffmpeg_error_msg;
|
||||
|
||||
pub mod info;
|
||||
|
||||
///
|
||||
/// Demuxer supports demuxing and decoding
|
||||
@ -20,8 +25,10 @@ use crate::pipeline::{PipelinePayload, PipelineStep};
|
||||
/// | Format | MPEG-TS |
|
||||
///
|
||||
pub(crate) struct Demuxer {
|
||||
buffer: BytesMut,
|
||||
ctx: *mut AVFormatContext,
|
||||
chan_in: UnboundedReceiver<Bytes>,
|
||||
chan_out: UnboundedSender<PipelinePayload>,
|
||||
started: Instant,
|
||||
}
|
||||
|
||||
unsafe impl Send for Demuxer {}
|
||||
@ -32,105 +39,141 @@ unsafe extern "C" fn read_data(
|
||||
buffer: *mut libc::c_uchar,
|
||||
size: libc::c_int,
|
||||
) -> libc::c_int {
|
||||
let muxer = opaque as *mut Demuxer;
|
||||
let len = size.min((*muxer).buffer.len() as libc::c_int);
|
||||
let chan = opaque as *mut UnboundedReceiver<Bytes>;
|
||||
let mut data = (*chan).blocking_recv().expect("shit");
|
||||
let buff_len = data.len();
|
||||
let mut len = size.min(buff_len as libc::c_int);
|
||||
|
||||
if len > 0 {
|
||||
memcpy(
|
||||
buffer as *mut libc::c_void,
|
||||
(*muxer).buffer.as_ptr() as *const libc::c_void,
|
||||
data.as_ptr() as *const libc::c_void,
|
||||
len as libc::c_ulonglong,
|
||||
);
|
||||
_ = (*muxer).buffer.split_to(len as usize);
|
||||
len
|
||||
} else {
|
||||
AVERROR_BUFFER_TOO_SMALL
|
||||
}
|
||||
len
|
||||
}
|
||||
|
||||
impl Demuxer {
|
||||
const BUFFER_SIZE: usize = 1024 * 1024;
|
||||
const INIT_BUFFER_THRESHOLD: usize = 2048;
|
||||
|
||||
pub fn new() -> Self {
|
||||
pub fn new(
|
||||
chan_in: UnboundedReceiver<Bytes>,
|
||||
chan_out: UnboundedSender<PipelinePayload>,
|
||||
) -> Self {
|
||||
unsafe {
|
||||
let ps = avformat_alloc_context();
|
||||
(*ps).probesize = Self::BUFFER_SIZE as i64;
|
||||
(*ps).flags |= AVFMT_FLAG_CUSTOM_IO;
|
||||
|
||||
Self {
|
||||
ctx: ps,
|
||||
buffer: BytesMut::with_capacity(Self::BUFFER_SIZE),
|
||||
chan_in,
|
||||
chan_out,
|
||||
started: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn append_buffer(&mut self, bytes: &Bytes) {
|
||||
self.buffer.extend_from_slice(bytes);
|
||||
}
|
||||
unsafe fn probe_input(&mut self) -> Result<DemuxStreamInfo, Error> {
|
||||
let buf_ptr = ptr::from_mut(&mut self.chan_in) as *mut libc::c_void;
|
||||
let pb = avio_alloc_context(
|
||||
av_mallocz(4096) as *mut libc::c_uchar,
|
||||
4096,
|
||||
0,
|
||||
buf_ptr,
|
||||
Some(read_data),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
unsafe fn probe_input(&mut self) -> Result<bool, Error> {
|
||||
let size = self.buffer.len();
|
||||
let score = (*self.ctx).probe_score;
|
||||
if score == 0 && size >= Self::INIT_BUFFER_THRESHOLD {
|
||||
let pb = avio_alloc_context(
|
||||
av_mallocz(4096) as *mut libc::c_uchar,
|
||||
4096,
|
||||
0,
|
||||
self as *const Self as *mut libc::c_void,
|
||||
Some(read_data),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
(*self.ctx).pb = pb;
|
||||
let ret = avformat_open_input(
|
||||
&mut self.ctx,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
);
|
||||
if ret < 0 {
|
||||
let msg = Self::get_ffmpeg_error_msg(ret);
|
||||
return Err(Error::msg(msg));
|
||||
}
|
||||
|
||||
if avformat_find_stream_info(self.ctx, ptr::null_mut()) < 0 {
|
||||
return Err(Error::msg("Could not find stream info"));
|
||||
}
|
||||
|
||||
for x in 0..(*self.ctx).nb_streams {
|
||||
av_dump_format(self.ctx, x as libc::c_int, ptr::null_mut(), 0);
|
||||
}
|
||||
}
|
||||
Ok(score > 0)
|
||||
}
|
||||
|
||||
unsafe fn decode_packet(&mut self) -> Result<Option<*mut AVPacket>, Error> {
|
||||
let pkt: *mut AVPacket = av_packet_alloc();
|
||||
av_init_packet(pkt);
|
||||
|
||||
let ret = av_read_frame(self.ctx, pkt);
|
||||
if ret == AVERROR_BUFFER_TOO_SMALL {
|
||||
return Ok(None);
|
||||
}
|
||||
(*self.ctx).pb = pb;
|
||||
let ret = avformat_open_input(
|
||||
&mut self.ctx,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
);
|
||||
if ret < 0 {
|
||||
let msg = Self::get_ffmpeg_error_msg(ret);
|
||||
let msg = get_ffmpeg_error_msg(ret);
|
||||
return Err(Error::msg(msg));
|
||||
}
|
||||
Ok(Some(pkt))
|
||||
if avformat_find_stream_info(self.ctx, ptr::null_mut()) < 0 {
|
||||
return Err(Error::msg("Could not find stream info"));
|
||||
}
|
||||
av_dump_format(self.ctx, 0, ptr::null_mut(), 0);
|
||||
|
||||
let mut channel_infos = vec![];
|
||||
let video_stream_index =
|
||||
av_find_best_stream(self.ctx, AVMEDIA_TYPE_VIDEO, -1, -1, ptr::null_mut(), 0) as usize;
|
||||
if video_stream_index != AVERROR_STREAM_NOT_FOUND as usize {
|
||||
let video_stream = *(*self.ctx).streams.add(video_stream_index);
|
||||
let codec_copy = unsafe {
|
||||
let ptr = avcodec_parameters_alloc();
|
||||
avcodec_parameters_copy(ptr, (*video_stream).codecpar);
|
||||
ptr
|
||||
};
|
||||
channel_infos.push(StreamInfoChannel {
|
||||
index: video_stream_index,
|
||||
channel_type: StreamChannelType::Video,
|
||||
width: (*(*video_stream).codecpar).width as usize,
|
||||
height: (*(*video_stream).codecpar).height as usize,
|
||||
codec_params: codec_copy,
|
||||
});
|
||||
}
|
||||
|
||||
let audio_stream_index =
|
||||
av_find_best_stream(self.ctx, AVMEDIA_TYPE_AUDIO, -1, -1, ptr::null_mut(), 0) as usize;
|
||||
if audio_stream_index != AVERROR_STREAM_NOT_FOUND as usize {
|
||||
let audio_stream = *(*self.ctx).streams.add(audio_stream_index);
|
||||
let codec_copy = unsafe {
|
||||
let ptr = avcodec_parameters_alloc();
|
||||
avcodec_parameters_copy(ptr, (*audio_stream).codecpar);
|
||||
ptr
|
||||
};
|
||||
channel_infos.push(StreamInfoChannel {
|
||||
index: audio_stream_index,
|
||||
channel_type: StreamChannelType::Audio,
|
||||
width: (*(*audio_stream).codecpar).width as usize,
|
||||
height: (*(*audio_stream).codecpar).height as usize,
|
||||
codec_params: codec_copy,
|
||||
});
|
||||
}
|
||||
|
||||
let info = DemuxStreamInfo {
|
||||
channels: channel_infos,
|
||||
};
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
unsafe fn print_buffer_info(&mut self) {
|
||||
let mut pb = (*self.ctx).pb;
|
||||
let offset = (*pb).pos;
|
||||
let remaining = (*pb).buffer_size as i64 - (*pb).pos;
|
||||
info!("offset={}, remaining={}", offset, remaining);
|
||||
unsafe fn get_packet(&mut self) -> Result<(), Error> {
|
||||
let pkt: *mut AVPacket = av_packet_alloc();
|
||||
let ret = av_read_frame(self.ctx, pkt);
|
||||
if ret == AVERROR_EOF {
|
||||
// reset EOF flag, stream never ends
|
||||
(*(*self.ctx).pb).eof_reached = 0;
|
||||
return Ok(());
|
||||
}
|
||||
if ret < 0 {
|
||||
let msg = get_ffmpeg_error_msg(ret);
|
||||
return Err(Error::msg(msg));
|
||||
}
|
||||
let stream = *(*self.ctx).streams.add((*pkt).stream_index as usize);
|
||||
(*pkt).time_base = (*stream).time_base;
|
||||
(*pkt).opaque = stream as *mut libc::c_void;
|
||||
|
||||
self.chan_out.send(PipelinePayload::AvPacket(pkt))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_ffmpeg_error_msg(ret: libc::c_int) -> String {
|
||||
pub fn process(&mut self) -> Result<Option<DemuxStreamInfo>, Error> {
|
||||
unsafe {
|
||||
let mut buf: [libc::c_char; 255] = [0; 255];
|
||||
av_make_error_string(buf.as_mut_ptr(), 255, ret);
|
||||
String::from(CStr::from_ptr(buf.as_ptr()).to_str().unwrap())
|
||||
let score = (*self.ctx).probe_score;
|
||||
if score < 30 {
|
||||
if (Instant::now() - self.started) > Duration::from_secs(1) {
|
||||
return Ok(Some(self.probe_input()?));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
self.get_packet()?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -143,32 +186,3 @@ impl Drop for Demuxer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PipelineStep for Demuxer {
|
||||
fn name(&self) -> String {
|
||||
"Demuxer".to_owned()
|
||||
}
|
||||
|
||||
async fn process(&mut self, pkg: PipelinePayload) -> Result<PipelinePayload, Error> {
|
||||
match pkg {
|
||||
PipelinePayload::Bytes(ref bb) => unsafe {
|
||||
self.append_buffer(bb);
|
||||
if !self.probe_input()? {
|
||||
return Ok(PipelinePayload::Empty);
|
||||
}
|
||||
match self.decode_packet() {
|
||||
Ok(pkt) => match pkt {
|
||||
Some(pkt) => Ok(PipelinePayload::AvPacket(pkt)),
|
||||
None => Ok(PipelinePayload::Empty),
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("{}", e);
|
||||
Ok(PipelinePayload::Empty)
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => return Err(Error::msg("Wrong pkg format")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
222
src/egress/hls.rs
Normal file
222
src/egress/hls.rs
Normal file
@ -0,0 +1,222 @@
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::mem::transmute;
|
||||
use std::ptr;
|
||||
use std::ptr::slice_from_raw_parts;
|
||||
|
||||
use crate::demux::info::{DemuxStreamInfo, StreamChannelType};
|
||||
use crate::fraction::Fraction;
|
||||
use anyhow::Error;
|
||||
use ffmpeg_sys_next::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE;
|
||||
use ffmpeg_sys_next::AVColorSpace::AVCOL_SPC_BT709;
|
||||
use ffmpeg_sys_next::AVMediaType::{AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO};
|
||||
use ffmpeg_sys_next::AVPixelFormat::AV_PIX_FMT_YUV420P;
|
||||
use ffmpeg_sys_next::AVSampleFormat::AV_SAMPLE_FMT_FLT;
|
||||
use ffmpeg_sys_next::{
|
||||
av_channel_layout_default, av_dump_format, av_interleaved_write_frame, av_opt_set,
|
||||
av_packet_rescale_ts, av_write_frame, avcodec_send_frame, avcodec_send_packet,
|
||||
avformat_alloc_output_context2, avformat_new_stream, avformat_write_header, AVChannelLayout,
|
||||
AVChannelLayout__bindgen_ty_1, AVCodecContext, AVFormatContext, AVPacket, AVRational,
|
||||
AV_CH_LAYOUT_STEREO,
|
||||
};
|
||||
use futures_util::StreamExt;
|
||||
use tokio::sync::mpsc::{Receiver, UnboundedReceiver};
|
||||
use log::info;
|
||||
use uuid::{Bytes, Uuid, Variant};
|
||||
|
||||
use crate::pipeline::{HLSEgressConfig, PipelinePayload};
|
||||
use crate::utils::get_ffmpeg_error_msg;
|
||||
use crate::variant::{VariantStream, VideoVariant};
|
||||
|
||||
pub struct HlsEgress {
|
||||
/// Pipeline id
|
||||
id: Uuid,
|
||||
config: HLSEgressConfig,
|
||||
ctx: *mut AVFormatContext,
|
||||
chan_in: UnboundedReceiver<PipelinePayload>,
|
||||
}
|
||||
|
||||
unsafe impl Send for HlsEgress {}
|
||||
unsafe impl Sync for HlsEgress {}
|
||||
|
||||
impl HlsEgress {
|
||||
pub fn new(chan_in: UnboundedReceiver<PipelinePayload>, id: Uuid, config: HLSEgressConfig) -> Self {
|
||||
Self {
|
||||
id,
|
||||
config,
|
||||
ctx: ptr::null_mut(),
|
||||
chan_in,
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn setup_muxer(&mut self) -> Result<(), Error> {
|
||||
let mut ctx = ptr::null_mut();
|
||||
|
||||
let ret = avformat_alloc_output_context2(
|
||||
&mut ctx,
|
||||
ptr::null(),
|
||||
"hls\0".as_ptr() as *const libc::c_char,
|
||||
format!("{}/stream_%v/live.m3u8\0", self.id).as_ptr() as *const libc::c_char,
|
||||
);
|
||||
if ret < 0 {
|
||||
return Err(Error::msg(get_ffmpeg_error_msg(ret)));
|
||||
}
|
||||
|
||||
av_opt_set(
|
||||
(*ctx).priv_data,
|
||||
"hls_segment_filename\0".as_ptr() as *const libc::c_char,
|
||||
format!("{}/stream_%v/seg_%05d.ts\0", self.id).as_ptr() as *const libc::c_char,
|
||||
0,
|
||||
);
|
||||
|
||||
av_opt_set(
|
||||
(*ctx).priv_data,
|
||||
"master_pl_name\0".as_ptr() as *const libc::c_char,
|
||||
"live.m3u8\0".as_ptr() as *const libc::c_char,
|
||||
0,
|
||||
);
|
||||
|
||||
av_opt_set(
|
||||
(*ctx).priv_data,
|
||||
"master_pl_publish_rate\0".as_ptr() as *const libc::c_char,
|
||||
"10\0".as_ptr() as *const libc::c_char,
|
||||
0,
|
||||
);
|
||||
|
||||
av_opt_set(
|
||||
(*ctx).priv_data,
|
||||
"hls_flags\0".as_ptr() as *const libc::c_char,
|
||||
"delete_segments\0".as_ptr() as *const libc::c_char,
|
||||
0,
|
||||
);
|
||||
|
||||
info!("map_str={}", self.config.stream_map);
|
||||
|
||||
av_opt_set(
|
||||
(*ctx).priv_data,
|
||||
"var_stream_map\0".as_ptr() as *const libc::c_char,
|
||||
format!("{}\0", self.config.stream_map).as_ptr() as *const libc::c_char,
|
||||
0,
|
||||
);
|
||||
|
||||
for var in &mut self.config.variants {
|
||||
match var {
|
||||
VariantStream::Video(vs) => {
|
||||
let stream = avformat_new_stream(ctx, ptr::null());
|
||||
if stream == ptr::null_mut() {
|
||||
return Err(Error::msg("Failed to add stream to output"));
|
||||
}
|
||||
|
||||
// overwrite dst_index to match output stream
|
||||
vs.dst_index = (*stream).index as usize;
|
||||
|
||||
let params = (*stream).codecpar;
|
||||
(*params).height = vs.height as libc::c_int;
|
||||
(*params).width = vs.width as libc::c_int;
|
||||
(*params).codec_id = transmute(vs.codec as i32);
|
||||
(*params).codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
(*params).format = AV_PIX_FMT_YUV420P as i32;
|
||||
(*params).framerate = AVRational {
|
||||
num: 1,
|
||||
den: vs.fps as libc::c_int,
|
||||
};
|
||||
(*params).bit_rate = vs.bitrate as i64;
|
||||
(*params).color_space = AVCOL_SPC_BT709;
|
||||
(*params).level = vs.level as libc::c_int;
|
||||
(*params).profile = vs.profile as libc::c_int;
|
||||
}
|
||||
VariantStream::Audio(va) => {
|
||||
let stream = avformat_new_stream(ctx, ptr::null());
|
||||
if stream == ptr::null_mut() {
|
||||
return Err(Error::msg("Failed to add stream to output"));
|
||||
}
|
||||
|
||||
// overwrite dst_index to match output stream
|
||||
va.dst_index = (*stream).index as usize;
|
||||
|
||||
let params = (*stream).codecpar;
|
||||
|
||||
(*params).codec_id = transmute(va.codec as i32);
|
||||
(*params).codec_type = AVMEDIA_TYPE_AUDIO;
|
||||
(*params).format = AV_SAMPLE_FMT_FLT as libc::c_int;
|
||||
(*params).bit_rate = va.bitrate as i64;
|
||||
(*params).sample_rate = va.sample_rate as libc::c_int;
|
||||
(*params).ch_layout = AVChannelLayout {
|
||||
order: AV_CHANNEL_ORDER_NATIVE,
|
||||
nb_channels: 2,
|
||||
u: AVChannelLayout__bindgen_ty_1 {
|
||||
mask: AV_CH_LAYOUT_STEREO,
|
||||
},
|
||||
opaque: ptr::null_mut(),
|
||||
};
|
||||
}
|
||||
_ => return Err(Error::msg("Invalid config")),
|
||||
}
|
||||
}
|
||||
|
||||
av_dump_format(ctx, 0, ptr::null(), 1);
|
||||
|
||||
let ret = avformat_write_header(ctx, ptr::null_mut());
|
||||
if ret < 0 {
|
||||
return Err(Error::msg(get_ffmpeg_error_msg(ret)));
|
||||
}
|
||||
|
||||
self.ctx = ctx;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
unsafe fn process_pkt(&mut self, pkt: *mut AVPacket) -> Result<(), Error> {
|
||||
let slice_raw = slice_from_raw_parts((*(*pkt).opaque_ref).data, 16);
|
||||
let binding = Bytes::from(*(slice_raw as *const [u8; 16]));
|
||||
let variant_id = Uuid::from_bytes_ref(&binding);
|
||||
let dst_stream_index = self.config.variants.iter().find_map(|v| match &v {
|
||||
VariantStream::Video(vv) => {
|
||||
if vv.id.eq(variant_id) {
|
||||
Some(vv.dst_index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
VariantStream::Audio(va) => {
|
||||
if va.id.eq(variant_id) {
|
||||
Some(va.dst_index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if let None = dst_stream_index {
|
||||
return Err(Error::msg(format!(
|
||||
"No stream found with id={:?}",
|
||||
dst_stream_index
|
||||
)));
|
||||
}
|
||||
|
||||
let stream = *(*self.ctx).streams.add(dst_stream_index.unwrap());
|
||||
av_packet_rescale_ts(pkt, (*pkt).time_base, (*stream).time_base);
|
||||
|
||||
(*pkt).stream_index = (*stream).index;
|
||||
|
||||
let ret = av_interleaved_write_frame(self.ctx, pkt);
|
||||
if ret < 0 {
|
||||
return Err(Error::msg(get_ffmpeg_error_msg(ret)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process(&mut self) -> Result<(), Error> {
|
||||
while let Ok(pkg) = self.chan_in.try_recv() {
|
||||
match pkg {
|
||||
PipelinePayload::AvPacket(pkt) => unsafe {
|
||||
if self.ctx == ptr::null_mut() {
|
||||
self.setup_muxer()?;
|
||||
}
|
||||
self.process_pkt(pkt)?;
|
||||
},
|
||||
_ => return Err(Error::msg("Payload not supported")),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
1
src/egress/mod.rs
Normal file
1
src/egress/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod hls;
|
185
src/encode/mod.rs
Normal file
185
src/encode/mod.rs
Normal file
@ -0,0 +1,185 @@
|
||||
use std::mem::transmute;
|
||||
use std::ptr;
|
||||
|
||||
use crate::ipc::Rx;
|
||||
use anyhow::Error;
|
||||
use async_trait::async_trait;
|
||||
use ffmpeg_sys_next::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE;
|
||||
use ffmpeg_sys_next::AVPixelFormat::AV_PIX_FMT_YUV420P;
|
||||
use ffmpeg_sys_next::AVSampleFormat::AV_SAMPLE_FMT_FLT;
|
||||
use ffmpeg_sys_next::{
|
||||
av_buffer_allocz, av_opt_set, av_packet_alloc, av_packet_free, avcodec_alloc_context3,
|
||||
avcodec_find_encoder, avcodec_open2, avcodec_receive_packet, avcodec_send_frame, memcpy,
|
||||
AVChannelLayout, AVChannelLayout__bindgen_ty_1, AVCodec, AVCodecContext, AVFrame, AVRational,
|
||||
AVERROR, AV_CH_LAYOUT_STEREO,
|
||||
};
|
||||
use libc::EAGAIN;
|
||||
use tokio::sync::mpsc::{UnboundedSender};
|
||||
|
||||
use crate::pipeline::PipelinePayload;
|
||||
use crate::utils::get_ffmpeg_error_msg;
|
||||
use crate::variant::VariantStream;
|
||||
|
||||
pub struct Encoder<T> {
|
||||
variant: VariantStream,
|
||||
ctx: *mut AVCodecContext,
|
||||
codec: *const AVCodec,
|
||||
chan_in: T,
|
||||
chan_out: UnboundedSender<PipelinePayload>,
|
||||
}
|
||||
|
||||
unsafe impl<T> Send for Encoder<T> {}
|
||||
unsafe impl<T> Sync for Encoder<T> {}
|
||||
|
||||
impl<TRecv> Encoder<TRecv>
|
||||
where
|
||||
TRecv: Rx<PipelinePayload>,
|
||||
{
|
||||
pub fn new(
|
||||
chan_in: TRecv,
|
||||
chan_out: UnboundedSender<PipelinePayload>,
|
||||
variant: VariantStream,
|
||||
) -> Self {
|
||||
Self {
|
||||
ctx: ptr::null_mut(),
|
||||
codec: ptr::null(),
|
||||
variant,
|
||||
chan_in,
|
||||
chan_out,
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn setup_encoder(&mut self, frame: *mut AVFrame) -> Result<(), Error> {
|
||||
if self.ctx == ptr::null_mut() {
|
||||
let codec = match &self.variant {
|
||||
VariantStream::Video(vv) => vv.codec,
|
||||
VariantStream::Audio(va) => va.codec,
|
||||
_ => return Err(Error::msg("Not supported")),
|
||||
};
|
||||
let encoder = avcodec_find_encoder(transmute(codec as i32));
|
||||
if encoder == ptr::null_mut() {
|
||||
return Err(Error::msg("Encoder not found"));
|
||||
}
|
||||
|
||||
let ctx = avcodec_alloc_context3(encoder);
|
||||
if ctx == ptr::null_mut() {
|
||||
return Err(Error::msg("Failed to allocate encoder context"));
|
||||
}
|
||||
|
||||
match &self.variant {
|
||||
VariantStream::Video(vv) => {
|
||||
(*ctx).bit_rate = vv.bitrate as i64;
|
||||
(*ctx).width = (*frame).width;
|
||||
(*ctx).height = (*frame).height;
|
||||
(*ctx).time_base = AVRational {
|
||||
num: 1,
|
||||
den: vv.fps as libc::c_int,
|
||||
};
|
||||
|
||||
(*ctx).gop_size = (vv.fps * vv.keyframe_interval) as libc::c_int;
|
||||
(*ctx).max_b_frames = 1;
|
||||
(*ctx).pix_fmt = AV_PIX_FMT_YUV420P;
|
||||
av_opt_set(
|
||||
(*ctx).priv_data,
|
||||
"preset\0".as_ptr() as *const libc::c_char,
|
||||
"fast\0".as_ptr() as *const libc::c_char,
|
||||
0,
|
||||
);
|
||||
}
|
||||
VariantStream::Audio(va) => {
|
||||
(*ctx).sample_fmt = if (*encoder).sample_fmts != ptr::null() {
|
||||
*(*encoder).sample_fmts.add(0)
|
||||
} else {
|
||||
AV_SAMPLE_FMT_FLT
|
||||
};
|
||||
(*ctx).bit_rate = va.bitrate as i64;
|
||||
(*ctx).sample_rate = va.sample_rate as libc::c_int;
|
||||
(*ctx).ch_layout = AVChannelLayout {
|
||||
order: AV_CHANNEL_ORDER_NATIVE,
|
||||
nb_channels: 2,
|
||||
u: AVChannelLayout__bindgen_ty_1 {
|
||||
mask: AV_CH_LAYOUT_STEREO,
|
||||
},
|
||||
opaque: ptr::null_mut(),
|
||||
};
|
||||
(*ctx).time_base = AVRational {
|
||||
num: 1,
|
||||
den: va.sample_rate as libc::c_int,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// nothing
|
||||
}
|
||||
};
|
||||
|
||||
let ret = avcodec_open2(ctx, encoder, ptr::null_mut());
|
||||
if ret < 0 {
|
||||
return Err(Error::msg(get_ffmpeg_error_msg(ret)));
|
||||
}
|
||||
|
||||
self.ctx = ctx;
|
||||
self.codec = encoder;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
unsafe fn process_frame(&mut self, frame: *mut AVFrame) -> Result<(), Error> {
|
||||
self.setup_encoder(frame)?;
|
||||
|
||||
let mut ret = avcodec_send_frame(self.ctx, frame);
|
||||
if ret < 0 {
|
||||
return Err(Error::msg(get_ffmpeg_error_msg(ret)));
|
||||
}
|
||||
|
||||
while ret > 0 {
|
||||
let mut pkt = av_packet_alloc();
|
||||
ret = avcodec_receive_packet(self.ctx, pkt);
|
||||
if ret < 0 {
|
||||
if ret == AVERROR(EAGAIN) {
|
||||
av_packet_free(&mut pkt);
|
||||
return Ok(());
|
||||
}
|
||||
return Err(Error::msg(get_ffmpeg_error_msg(ret)));
|
||||
}
|
||||
|
||||
(*pkt).duration = (*frame).duration;
|
||||
(*pkt).time_base = (*frame).time_base;
|
||||
(*pkt).opaque_ref = match &self.variant {
|
||||
VariantStream::Audio(va) => {
|
||||
let buf = av_buffer_allocz(16);
|
||||
memcpy(
|
||||
(*buf).data as *mut libc::c_void,
|
||||
va.id.as_bytes().as_ptr() as *const libc::c_void,
|
||||
16,
|
||||
);
|
||||
buf
|
||||
}
|
||||
VariantStream::Video(vv) => {
|
||||
let buf = av_buffer_allocz(16);
|
||||
memcpy(
|
||||
(*buf).data as *mut libc::c_void,
|
||||
vv.id.as_bytes().as_ptr() as *const libc::c_void,
|
||||
16,
|
||||
);
|
||||
buf
|
||||
}
|
||||
_ => return Err(Error::msg("Cannot assign pkt stream index")),
|
||||
};
|
||||
self.chan_out.send(PipelinePayload::AvPacket(pkt))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process(&mut self) -> Result<(), Error> {
|
||||
while let Ok(pkg) = self.chan_in.try_recv() {
|
||||
match pkg {
|
||||
PipelinePayload::AvFrame(frm) => unsafe {
|
||||
self.process_frame(frm)?;
|
||||
},
|
||||
_ => return Err(Error::msg("Payload not supported")),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
34
src/fraction.rs
Normal file
34
src/fraction.rs
Normal file
@ -0,0 +1,34 @@
|
||||
#[derive(Clone, Debug, Copy)]
|
||||
pub struct Fraction {
|
||||
pub num: usize,
|
||||
pub den: usize,
|
||||
}
|
||||
|
||||
fn gcd(mut a: usize, mut b: usize) -> usize {
|
||||
if a == b {
|
||||
return a;
|
||||
}
|
||||
if b > a {
|
||||
std::mem::swap(&mut a, &mut b);
|
||||
}
|
||||
while b > 0 {
|
||||
let temp = a;
|
||||
a = b;
|
||||
b = temp % b;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
impl From<(usize, usize)> for Fraction {
|
||||
fn from(value: (usize, usize)) -> Self {
|
||||
let mut num = value.0;
|
||||
let mut den = value.1;
|
||||
|
||||
let gcd = gcd(num, den);
|
||||
|
||||
Self {
|
||||
num: num / gcd,
|
||||
den: den / gcd,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,13 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod srt;
|
||||
pub mod tcp;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ConnectionInfo {
|
||||
/// Endpoint of the ingress
|
||||
pub endpoint: String,
|
||||
|
||||
/// IP address of the connection
|
||||
pub ip_addr: String,
|
||||
}
|
@ -4,51 +4,39 @@ use crate::pipeline::runner::PipelineRunner;
|
||||
use futures_util::{StreamExt, TryStreamExt};
|
||||
use log::{info, warn};
|
||||
use srt_tokio::{SrtListener, SrtSocket};
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
pub async fn listen_srt(port: u16, pipeline: PipelineBuilder) -> Result<(), anyhow::Error> {
|
||||
let (_binding, mut packets) = SrtListener::builder().bind(port).await?;
|
||||
pub async fn listen(addr: String, builder: PipelineBuilder) -> Result<(), anyhow::Error> {
|
||||
let (_binding, mut packets) = SrtListener::builder().bind(addr.clone()).await?;
|
||||
|
||||
info!("SRT listening on: {}", addr.clone());
|
||||
while let Some(request) = packets.incoming().next().await {
|
||||
let mut socket = request.accept(None).await?;
|
||||
let pipeline = pipeline.clone();
|
||||
tokio::spawn(async move {
|
||||
let info = ConnectionInfo {};
|
||||
if let Ok(pl) = pipeline.build_for(info).await {
|
||||
let mut stream = SrtStream::new(socket);
|
||||
stream.run(pl).await;
|
||||
} else {
|
||||
let ep = addr.clone();
|
||||
let info = ConnectionInfo {
|
||||
endpoint: ep.clone(),
|
||||
ip_addr: socket.settings().remote.to_string(),
|
||||
};
|
||||
let (tx, rx) = unbounded_channel();
|
||||
if let Ok(mut pipeline) = builder.build_for(info, rx).await {
|
||||
std::thread::spawn(move || loop {
|
||||
if let Err(e) = pipeline.run() {
|
||||
warn!("Pipeline error: {}\n{}", e, e.backtrace());
|
||||
break;
|
||||
}
|
||||
});
|
||||
tokio::spawn(async move {
|
||||
info!("New client connected: {}", ep);
|
||||
while let Ok(Some((_inst, bytes))) = socket.try_next().await {
|
||||
if let Err(e) = tx.send(bytes) {
|
||||
warn!("SRT Error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
socket.close_and_finish().await.unwrap();
|
||||
}
|
||||
});
|
||||
info!("Client {} disconnected", ep);
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct SrtStream {
|
||||
socket: SrtSocket,
|
||||
prev: Option<(bytes::Bytes, usize)>,
|
||||
}
|
||||
|
||||
impl SrtStream {
|
||||
pub fn new(socket: SrtSocket) -> Self {
|
||||
Self { socket, prev: None }
|
||||
}
|
||||
|
||||
pub async fn run(&mut self, mut pipeline: PipelineRunner) {
|
||||
let socket_id = self.socket.settings().remote_sockid.0;
|
||||
let client_desc = format!(
|
||||
"(ip_port: {}, socket_id: {}, stream_id: {:?})",
|
||||
self.socket.settings().remote,
|
||||
socket_id,
|
||||
self.socket.settings().stream_id,
|
||||
);
|
||||
info!("New client connected: {}", client_desc);
|
||||
while let Ok(Some((_inst, bytes))) = self.socket.try_next().await {
|
||||
if let Err(e) = pipeline.push(bytes).await {
|
||||
warn!("{:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
info!("Client {} disconnected", client_desc);
|
||||
}
|
||||
}
|
||||
|
61
src/ingress/tcp.rs
Normal file
61
src/ingress/tcp.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use crate::ingress::ConnectionInfo;
|
||||
use crate::pipeline::builder::PipelineBuilder;
|
||||
use crate::pipeline::runner::PipelineRunner;
|
||||
use bytes::BytesMut;
|
||||
use futures_util::{StreamExt, TryStreamExt};
|
||||
use log::{error, info, warn};
|
||||
use srt_tokio::{SrtListener, SrtSocket};
|
||||
use std::io;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::net::{TcpListener, TcpSocket};
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
pub async fn listen(addr: String, builder: PipelineBuilder) -> Result<(), anyhow::Error> {
|
||||
let listener = TcpListener::bind(addr.clone()).await.unwrap();
|
||||
|
||||
info!("TCP listening on: {}", addr.clone());
|
||||
while let Ok((mut socket, ip)) = listener.accept().await {
|
||||
info!("New client connected: {}", ip.clone());
|
||||
let ep = addr.clone();
|
||||
let builder = builder.clone();
|
||||
tokio::spawn(async move {
|
||||
let (sender, recv) = unbounded_channel();
|
||||
let info = ConnectionInfo {
|
||||
ip_addr: ip.to_string(),
|
||||
endpoint: ep,
|
||||
};
|
||||
|
||||
if let Ok(mut pl) = builder.build_for(info, recv).await {
|
||||
std::thread::spawn(move || loop {
|
||||
if let Err(e) = pl.run() {
|
||||
warn!("Pipeline error: {}", e.backtrace());
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match socket.read(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
let bytes = bytes::Bytes::copy_from_slice(&buf[0..n]);
|
||||
if let Err(e) = sender.send(bytes) {
|
||||
warn!("{:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Client disconnected: {}", ip);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
35
src/ipc.rs
Normal file
35
src/ipc.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use anyhow::Error;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Rx<T> {
|
||||
async fn recv(&mut self) -> Result<T, Error>;
|
||||
fn try_recv(&mut self) -> Result<T, Error>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> Rx<T> for tokio::sync::mpsc::UnboundedReceiver<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
async fn recv(&mut self) -> Result<T, Error> {
|
||||
self.recv().await.ok_or(Error::msg("recv error"))
|
||||
}
|
||||
|
||||
fn try_recv(&mut self) -> Result<T, Error> {
|
||||
Ok(self.try_recv()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> Rx<T> for tokio::sync::broadcast::Receiver<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
async fn recv(&mut self) -> Result<T, Error> {
|
||||
Ok(self.recv().await?)
|
||||
}
|
||||
fn try_recv(&mut self) -> Result<T, Error> {
|
||||
Ok(self.try_recv()?)
|
||||
}
|
||||
}
|
67
src/main.rs
67
src/main.rs
@ -1,13 +1,27 @@
|
||||
mod pipeline;
|
||||
mod ingress;
|
||||
mod webhook;
|
||||
mod decode;
|
||||
mod demux;
|
||||
mod egress;
|
||||
mod encode;
|
||||
mod fraction;
|
||||
mod ingress;
|
||||
mod pipeline;
|
||||
mod scale;
|
||||
mod settings;
|
||||
mod utils;
|
||||
mod variant;
|
||||
mod webhook;
|
||||
mod ipc;
|
||||
|
||||
use std::ffi::CStr;
|
||||
use futures_util::StreamExt;
|
||||
use log::info;
|
||||
use crate::pipeline::builder::PipelineBuilder;
|
||||
use crate::settings::Settings;
|
||||
use crate::webhook::Webhook;
|
||||
use config::Config;
|
||||
use futures_util::StreamExt;
|
||||
use log::{error, info};
|
||||
use std::ffi::CStr;
|
||||
use futures_util::future::join_all;
|
||||
use tokio::sync::futures;
|
||||
use url::Url;
|
||||
|
||||
/// Test: ffmpeg -re -f lavfi -i testsrc -g 2 -r 30 -pix_fmt yuv420p -s 1280x720 -c:v h264 -b:v 2000k -f mpegts srt://localhost:3333
|
||||
#[tokio::main]
|
||||
@ -15,16 +29,41 @@ async fn main() -> anyhow::Result<()> {
|
||||
pretty_env_logger::init();
|
||||
|
||||
unsafe {
|
||||
ffmpeg_sys_next::av_log_set_level(ffmpeg_sys_next::AV_LOG_INFO);
|
||||
info!("{}", CStr::from_ptr(ffmpeg_sys_next::av_version_info()).to_str().unwrap());
|
||||
//ffmpeg_sys_next::av_log_set_level(ffmpeg_sys_next::AV_LOG_MAX_OFFSET);
|
||||
info!(
|
||||
"FFMPEG version={}",
|
||||
CStr::from_ptr(ffmpeg_sys_next::av_version_info())
|
||||
.to_str()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
let webhook = Webhook::new("".to_owned());
|
||||
let builder = Config::builder()
|
||||
.add_source(config::File::with_name("config.toml"))
|
||||
.add_source(config::Environment::with_prefix("APP"))
|
||||
.build()?;
|
||||
|
||||
let settings: Settings = builder.try_deserialize()?;
|
||||
|
||||
let webhook = Webhook::new(settings.webhook_url);
|
||||
let builder = PipelineBuilder::new(webhook);
|
||||
let srt = tokio::spawn(ingress::srt::listen_srt(3333, builder));
|
||||
|
||||
srt.await?.expect("TODO: panic message");
|
||||
|
||||
println!("\nServer closed");
|
||||
let mut listeners = vec![];
|
||||
for e in settings.endpoints {
|
||||
let u: Url = e.parse()?;
|
||||
let addr = format!("{}:{}", u.host_str().unwrap(), u.port().unwrap());
|
||||
match u.scheme() {
|
||||
"srt" => listeners.push(tokio::spawn(ingress::srt::listen(addr, builder.clone()))),
|
||||
"tcp" => listeners.push(tokio::spawn(ingress::tcp::listen(addr, builder.clone()))),
|
||||
_ => {
|
||||
error!("Unknown endpoint config: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
for handle in listeners {
|
||||
if let Err(e) = handle.await {
|
||||
error!("{e}");
|
||||
}
|
||||
}
|
||||
info!("Server closed");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
use crate::demux::Demuxer;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use crate::ingress::ConnectionInfo;
|
||||
use crate::pipeline::runner::PipelineRunner;
|
||||
use crate::pipeline::PipelineStep;
|
||||
use crate::webhook::Webhook;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -14,12 +13,8 @@ impl PipelineBuilder {
|
||||
Self { webhook }
|
||||
}
|
||||
|
||||
pub async fn build_for(&self, info: ConnectionInfo) -> Result<PipelineRunner, anyhow::Error> {
|
||||
pub async fn build_for(&self, info: ConnectionInfo, recv: UnboundedReceiver<bytes::Bytes>) -> Result<PipelineRunner, anyhow::Error> {
|
||||
let config = self.webhook.start(info).await?;
|
||||
|
||||
let mut steps: Vec<Box<dyn PipelineStep + Sync + Send>> = Vec::new();
|
||||
steps.push(Box::new(Demuxer::new()));
|
||||
|
||||
Ok(PipelineRunner::new(steps))
|
||||
Ok(PipelineRunner::new(config, recv))
|
||||
}
|
||||
}
|
||||
|
@ -1,42 +1,58 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use ffmpeg_sys_next::{av_packet_unref, AVPacket};
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use ffmpeg_sys_next::{AVFrame, AVPacket};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::demux::info::DemuxStreamInfo;
|
||||
use crate::variant::VariantStream;
|
||||
|
||||
pub mod builder;
|
||||
pub mod runner;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum EgressType {
|
||||
HLS(HLSEgressConfig),
|
||||
DASH,
|
||||
WHEP,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct HLSEgressConfig {
|
||||
pub variants: Vec<VariantStream>,
|
||||
|
||||
/// FFMPEG stream mapping string
|
||||
///
|
||||
/// v:0,a:0 v:1,a:0, v:2,a:1 etc..
|
||||
pub stream_map: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PipelineConfig {
|
||||
pub id: uuid::Uuid,
|
||||
pub recording: Vec<VariantStream>,
|
||||
pub egress: Vec<EgressType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PipelinePayload {
|
||||
/// No output
|
||||
Empty,
|
||||
/// Skip this step
|
||||
Skip,
|
||||
/// Raw bytes from ingress
|
||||
Bytes(bytes::Bytes),
|
||||
/// FFMpeg AVPacket
|
||||
AvPacket(*mut AVPacket),
|
||||
/// FFMpeg AVFrame
|
||||
AvFrame(),
|
||||
AvFrame(*mut AVFrame),
|
||||
/// Information about the input stream
|
||||
SourceInfo(DemuxStreamInfo),
|
||||
}
|
||||
|
||||
unsafe impl Send for PipelinePayload {}
|
||||
unsafe impl Sync for PipelinePayload {}
|
||||
|
||||
impl Drop for PipelinePayload {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
PipelinePayload::AvPacket(pkt) => unsafe {
|
||||
av_packet_unref(*pkt);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait PipelineStep {
|
||||
fn name(&self) -> String;
|
||||
|
||||
async fn process(&mut self, pkg: PipelinePayload) -> Result<PipelinePayload, anyhow::Error>;
|
||||
async fn process(&mut self, pkg: &PipelinePayload) -> Result<PipelinePayload, anyhow::Error>;
|
||||
}
|
||||
|
@ -1,20 +1,112 @@
|
||||
use crate::pipeline::{PipelinePayload, PipelineStep};
|
||||
use crate::decode::Decoder;
|
||||
use crate::demux::info::{DemuxStreamInfo, StreamChannelType};
|
||||
use crate::demux::Demuxer;
|
||||
use crate::egress::hls::HlsEgress;
|
||||
use crate::encode::Encoder;
|
||||
use crate::pipeline::{EgressType, PipelineConfig, PipelinePayload, PipelineStep};
|
||||
use crate::scale::Scaler;
|
||||
use crate::variant::VariantStream;
|
||||
use anyhow::Error;
|
||||
use log::info;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
|
||||
|
||||
struct ScalerEncoder {
|
||||
pub scaler: Scaler,
|
||||
pub encoder: Encoder<UnboundedReceiver<PipelinePayload>>,
|
||||
}
|
||||
|
||||
pub struct PipelineRunner {
|
||||
steps: Vec<Box<dyn PipelineStep + Sync + Send>>,
|
||||
config: PipelineConfig,
|
||||
demuxer: Demuxer,
|
||||
decoder: Decoder,
|
||||
decoder_output: broadcast::Receiver<PipelinePayload>,
|
||||
scalers: Vec<ScalerEncoder>,
|
||||
encoders: Vec<Encoder<broadcast::Receiver<PipelinePayload>>>,
|
||||
egress: Vec<HlsEgress>,
|
||||
}
|
||||
|
||||
impl PipelineRunner {
|
||||
pub fn new(steps: Vec<Box<dyn PipelineStep + Sync + Send>>) -> Self {
|
||||
Self { steps }
|
||||
pub fn new(config: PipelineConfig, recv: UnboundedReceiver<bytes::Bytes>) -> Self {
|
||||
let (demux_out, demux_in) = unbounded_channel();
|
||||
let (dec_tx, dec_rx) = broadcast::channel::<PipelinePayload>(32);
|
||||
Self {
|
||||
config,
|
||||
demuxer: Demuxer::new(recv, demux_out),
|
||||
decoder: Decoder::new(demux_in, dec_tx),
|
||||
decoder_output: dec_rx,
|
||||
scalers: vec![],
|
||||
encoders: vec![],
|
||||
egress: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn push(&mut self, bytes: bytes::Bytes) -> Result<(), anyhow::Error> {
|
||||
let mut output = PipelinePayload::Bytes(bytes);
|
||||
for step in &mut self.steps {
|
||||
match step.process(output).await? {
|
||||
Some(pkg) => output = pkg,
|
||||
None => break,
|
||||
pub fn run(&mut self) -> Result<(), Error> {
|
||||
if let Some(cfg) = self.demuxer.process()? {
|
||||
self.configure_pipeline(cfg)?;
|
||||
}
|
||||
self.decoder.process()?;
|
||||
for sw in &mut self.scalers {
|
||||
sw.scaler.process()?;
|
||||
sw.encoder.process()?;
|
||||
for eg in &mut self.egress {
|
||||
eg.process()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_pipeline(&mut self, info: DemuxStreamInfo) -> Result<(), Error> {
|
||||
// configure scalers
|
||||
if self.scalers.len() != 0 {
|
||||
return Err(Error::msg("Pipeline already configured!"));
|
||||
}
|
||||
info!("Configuring pipeline {:?}", info);
|
||||
|
||||
let video_stream = info
|
||||
.channels
|
||||
.iter()
|
||||
.find(|s| s.channel_type == StreamChannelType::Video);
|
||||
|
||||
if let Some(ref vs) = video_stream {
|
||||
for eg in &self.config.egress {
|
||||
match eg {
|
||||
EgressType::HLS(cfg) => {
|
||||
let (egress_tx, egress_rx) = unbounded_channel();
|
||||
self.egress
|
||||
.push(HlsEgress::new(egress_rx, self.config.id, cfg.clone()));
|
||||
|
||||
for v in &cfg.variants {
|
||||
let (var_tx, var_rx) = unbounded_channel();
|
||||
match v {
|
||||
VariantStream::Video(vs) => {
|
||||
self.scalers.push(ScalerEncoder {
|
||||
scaler: Scaler::new(
|
||||
self.decoder_output.resubscribe(),
|
||||
var_tx.clone(),
|
||||
vs.clone(),
|
||||
),
|
||||
encoder: Encoder::new(var_rx, egress_tx.clone(), v.clone()),
|
||||
});
|
||||
}
|
||||
VariantStream::Audio(_) => {
|
||||
self.encoders.push(Encoder::new(
|
||||
self.decoder_output.resubscribe(),
|
||||
egress_tx.clone(),
|
||||
v.clone(),
|
||||
));
|
||||
}
|
||||
c => {
|
||||
return Err(Error::msg(format!(
|
||||
"Variant config not supported {:?}",
|
||||
c
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return Err(Error::msg("Egress config not supported")),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
96
src/scale/mod.rs
Normal file
96
src/scale/mod.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use std::mem::transmute;
|
||||
use std::ptr;
|
||||
|
||||
use anyhow::Error;
|
||||
use ffmpeg_sys_next::{
|
||||
av_frame_alloc, av_frame_copy_props, av_frame_unref, AVFrame, SWS_BILINEAR, sws_getContext,
|
||||
sws_scale_frame, SwsContext,
|
||||
};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::pipeline::PipelinePayload;
|
||||
use crate::utils::get_ffmpeg_error_msg;
|
||||
use crate::variant::VideoVariant;
|
||||
|
||||
pub struct Scaler {
|
||||
variant: VideoVariant,
|
||||
ctx: *mut SwsContext,
|
||||
chan_in: broadcast::Receiver<PipelinePayload>,
|
||||
chan_out: UnboundedSender<PipelinePayload>,
|
||||
}
|
||||
|
||||
unsafe impl Send for Scaler {}
|
||||
unsafe impl Sync for Scaler {}
|
||||
|
||||
impl Scaler {
|
||||
pub fn new(
|
||||
chan_in: broadcast::Receiver<PipelinePayload>,
|
||||
chan_out: UnboundedSender<PipelinePayload>,
|
||||
variant: VideoVariant,
|
||||
) -> Self {
|
||||
Self {
|
||||
chan_in,
|
||||
chan_out,
|
||||
variant,
|
||||
ctx: ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn process_frame(&mut self, frame: *mut AVFrame) -> Result<(), Error> {
|
||||
if (*frame).width == 0 {
|
||||
// only picture frames supported
|
||||
return Ok(());
|
||||
}
|
||||
let dst_fmt = transmute((*frame).format);
|
||||
|
||||
if self.ctx == ptr::null_mut() {
|
||||
let ctx = sws_getContext(
|
||||
(*frame).width,
|
||||
(*frame).height,
|
||||
dst_fmt,
|
||||
self.variant.width as libc::c_int,
|
||||
self.variant.height as libc::c_int,
|
||||
dst_fmt,
|
||||
SWS_BILINEAR,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
);
|
||||
if ctx == ptr::null_mut() {
|
||||
return Err(Error::msg("Failed to create scalar context"));
|
||||
}
|
||||
self.ctx = ctx;
|
||||
}
|
||||
|
||||
let dst_frame = av_frame_alloc();
|
||||
let ret = av_frame_copy_props(dst_frame, frame);
|
||||
if ret < 0 {
|
||||
return Err(Error::msg(get_ffmpeg_error_msg(ret)));
|
||||
}
|
||||
|
||||
let ret = sws_scale_frame(self.ctx, dst_frame, frame);
|
||||
av_frame_unref(frame);
|
||||
if ret < 0 {
|
||||
return Err(Error::msg(get_ffmpeg_error_msg(ret)));
|
||||
}
|
||||
|
||||
(*dst_frame).time_base = (*frame).time_base;
|
||||
(*dst_frame).pts = (*frame).pts;
|
||||
(*dst_frame).pkt_dts = (*frame).pkt_dts;
|
||||
self.chan_out.send(PipelinePayload::AvFrame(dst_frame))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process(&mut self) -> Result<(), Error> {
|
||||
while let Ok(pkg) = self.chan_in.try_recv() {
|
||||
match pkg {
|
||||
PipelinePayload::AvFrame(frm) => unsafe {
|
||||
self.process_frame(frm)?;
|
||||
},
|
||||
_ => return Err(Error::msg("Payload not supported payload")),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
17
src/settings.rs
Normal file
17
src/settings.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
/// List of listen endpoints
|
||||
///
|
||||
/// - srt://localhost:3333
|
||||
/// - tcp://localhost:3334
|
||||
/// - rtmp://localhost:1935
|
||||
pub endpoints: Vec<String>,
|
||||
|
||||
/// Output directory for egress
|
||||
pub output_dir: String,
|
||||
|
||||
/// Webhook configuration URL
|
||||
pub webhook_url: String,
|
||||
}
|
11
src/utils.rs
Normal file
11
src/utils.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use ffmpeg_sys_next::av_make_error_string;
|
||||
use std::ffi::CStr;
|
||||
|
||||
pub fn get_ffmpeg_error_msg(ret: libc::c_int) -> String {
|
||||
unsafe {
|
||||
const BUF_SIZE: usize = 512;
|
||||
let mut buf: [libc::c_char; BUF_SIZE] = [0; BUF_SIZE];
|
||||
av_make_error_string(buf.as_mut_ptr(), BUF_SIZE, ret);
|
||||
String::from(CStr::from_ptr(buf.as_ptr()).to_str().unwrap())
|
||||
}
|
||||
}
|
104
src/variant.rs
Normal file
104
src/variant.rs
Normal file
@ -0,0 +1,104 @@
|
||||
use crate::fraction::Fraction;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum VariantStream {
|
||||
/// Video stream mapping
|
||||
Video(VideoVariant),
|
||||
/// Audio stream mapping
|
||||
Audio(AudioVariant),
|
||||
/// Copy source stream (video)
|
||||
CopyVideo(usize),
|
||||
/// Copy source stream (audio)
|
||||
CopyAudio(usize),
|
||||
}
|
||||
|
||||
/// Information related to variant streams for a given egress
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct VideoVariant {
|
||||
/// Unique ID of this variant
|
||||
pub id: Uuid,
|
||||
|
||||
/// Source video stream to use for this variant
|
||||
pub src_index: usize,
|
||||
|
||||
/// Index of this variant in the output
|
||||
pub dst_index: usize,
|
||||
|
||||
/// Width of this video stream
|
||||
pub width: u16,
|
||||
|
||||
/// Height of this video stream
|
||||
pub height: u16,
|
||||
|
||||
/// FPS for this stream
|
||||
pub fps: u16,
|
||||
|
||||
/// Bitrate of this stream
|
||||
pub bitrate: u64,
|
||||
|
||||
/// AVCodecID
|
||||
pub codec: usize,
|
||||
|
||||
/// Codec profile
|
||||
pub profile: usize,
|
||||
|
||||
/// Codec level
|
||||
pub level: usize,
|
||||
|
||||
/// Keyframe interval in seconds
|
||||
pub keyframe_interval: u16,
|
||||
}
|
||||
|
||||
impl Display for VideoVariant {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Video #{}: {}, {}p, {}fps, {}kbps",
|
||||
self.src_index,
|
||||
self.codec,
|
||||
self.height,
|
||||
self.fps,
|
||||
self.bitrate / 1000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Information related to variant streams for a given egress
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AudioVariant {
|
||||
/// Unique ID of this variant
|
||||
pub id: Uuid,
|
||||
|
||||
/// Source video stream to use for this variant
|
||||
pub src_index: usize,
|
||||
|
||||
/// Index of this variant in the output
|
||||
pub dst_index: usize,
|
||||
|
||||
/// Bitrate of this stream
|
||||
pub bitrate: u64,
|
||||
|
||||
/// AVCodecID
|
||||
pub codec: usize,
|
||||
|
||||
/// Number of channels
|
||||
pub channels: u16,
|
||||
|
||||
/// Sample rate
|
||||
pub sample_rate: usize,
|
||||
}
|
||||
|
||||
impl Display for AudioVariant {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Audio #{}: {}, {}kbps",
|
||||
self.src_index,
|
||||
self.codec,
|
||||
self.bitrate / 1000
|
||||
)
|
||||
}
|
||||
}
|
@ -1,7 +1,13 @@
|
||||
use crate::ingress::ConnectionInfo;
|
||||
use crate::pipeline::{EgressType, HLSEgressConfig, PipelineConfig};
|
||||
use crate::variant::{AudioVariant, VariantStream, VideoVariant};
|
||||
use ffmpeg_sys_next::AVCodecID::{AV_CODEC_ID_AAC, AV_CODEC_ID_H264};
|
||||
use ffmpeg_sys_next::{AV_LEVEL_UNKNOWN, AV_PROFILE_H264_HIGH};
|
||||
use std::fmt::Display;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Webhook {
|
||||
pub struct Webhook {
|
||||
url: String,
|
||||
}
|
||||
|
||||
@ -10,9 +16,68 @@ impl Webhook {
|
||||
Self { url }
|
||||
}
|
||||
|
||||
pub async fn start(&self, connection_info: ConnectionInfo) -> Result<PipelineConfig, anyhow::Error> {
|
||||
Ok(PipelineConfig {})
|
||||
pub async fn start(
|
||||
&self,
|
||||
connection_info: ConnectionInfo,
|
||||
) -> Result<PipelineConfig, anyhow::Error> {
|
||||
let video_var = VideoVariant {
|
||||
id: Uuid::new_v4(),
|
||||
src_index: 0,
|
||||
dst_index: 0,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 30,
|
||||
bitrate: 3_000_000,
|
||||
codec: 27,
|
||||
profile: 100,
|
||||
level: 1,
|
||||
keyframe_interval: 2,
|
||||
};
|
||||
let video_var_2 = VideoVariant {
|
||||
id: Uuid::new_v4(),
|
||||
src_index: 0,
|
||||
dst_index: 0,
|
||||
width: 640,
|
||||
height: 360,
|
||||
fps: 30,
|
||||
bitrate: 1_000_000,
|
||||
codec: 27,
|
||||
profile: 100,
|
||||
level: 1,
|
||||
keyframe_interval: 2,
|
||||
};
|
||||
let audio_var = AudioVariant {
|
||||
id: Uuid::new_v4(),
|
||||
src_index: 1,
|
||||
dst_index: 0,
|
||||
bitrate: 320_000,
|
||||
codec: 86018,
|
||||
channels: 2,
|
||||
sample_rate: 44_100,
|
||||
};
|
||||
|
||||
let audio_var_2 = AudioVariant {
|
||||
id: Uuid::new_v4(),
|
||||
src_index: 1,
|
||||
dst_index: 0,
|
||||
bitrate: 220_000,
|
||||
codec: 86018,
|
||||
channels: 2,
|
||||
sample_rate: 44_100,
|
||||
};
|
||||
|
||||
Ok(PipelineConfig {
|
||||
id: Uuid::new_v4(),
|
||||
egress: vec![EgressType::HLS(HLSEgressConfig {
|
||||
variants: vec![
|
||||
VariantStream::Video(video_var),
|
||||
VariantStream::Video(video_var_2),
|
||||
VariantStream::Audio(audio_var),
|
||||
VariantStream::Audio(audio_var_2),
|
||||
],
|
||||
stream_map: "v:0,a:0 v:1,a:1".to_owned(),
|
||||
})],
|
||||
recording: vec![VariantStream::CopyVideo(0), VariantStream::CopyAudio(1)],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PipelineConfig {}
|
||||
|
Reference in New Issue
Block a user