Configurable encoder pipeline

This commit is contained in:
Kieran 2024-03-20 22:46:19 +00:00
parent 13cb456f89
commit 529e3b6234
24 changed files with 1707 additions and 209 deletions

349
Cargo.lock generated
View File

@ -42,6 +42,9 @@ name = "anyhow"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
dependencies = [
"backtrace",
]
[[package]]
name = "array-init"
@ -87,6 +90,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "bindgen"
version = "0.64.0"
@ -118,6 +127,9 @@ name = "bitflags"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
dependencies = [
"serde",
]
[[package]]
name = "block-buffer"
@ -176,12 +188,61 @@ dependencies = [
"libloading",
]
[[package]]
name = "config"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be"
dependencies = [
"async-trait",
"convert_case 0.6.0",
"json5",
"lazy_static",
"nom",
"pathdiff",
"ron",
"rust-ini",
"serde",
"serde_json",
"toml",
"yaml-rust",
]
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cpufeatures"
version = "0.2.12"
@ -191,6 +252,12 @@ dependencies = [
"libc",
]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -216,7 +283,7 @@ version = "0.99.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
dependencies = [
"convert_case",
"convert_case 0.4.0",
"proc-macro2",
"quote",
"rustc_version",
@ -234,6 +301,15 @@ dependencies = [
"subtle",
]
[[package]]
name = "dlv-list"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
dependencies = [
"const-random",
]
[[package]]
name = "env_logger"
version = "0.10.2"
@ -386,6 +462,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
[[package]]
name = "hashbrown"
version = "0.14.3"
@ -436,7 +518,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.14.3",
]
[[package]]
@ -459,6 +541,23 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "json5"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
dependencies = [
"pest",
"pest_derive",
"serde",
]
[[package]]
name = "keyed_priority_queue"
version = "0.4.2"
@ -496,6 +595,12 @@ dependencies = [
"windows-targets 0.52.4",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "log"
version = "0.4.21"
@ -572,6 +677,28 @@ dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "ordered-multimap"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e"
dependencies = [
"dlv-list",
"hashbrown 0.13.2",
]
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pbkdf2"
version = "0.12.2"
@ -593,6 +720,51 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
version = "2.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8"
dependencies = [
"memchr",
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.52",
]
[[package]]
name = "pest_meta"
version = "2.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
@ -617,6 +789,12 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "pretty-hex"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbc83ee4a840062f368f9096d80077a9841ec117e17e7f700df81958f1451254"
[[package]]
name = "pretty_env_logger"
version = "0.5.0"
@ -704,6 +882,28 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "ron"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
dependencies = [
"base64",
"bitflags 2.4.2",
"serde",
"serde_derive",
]
[[package]]
name = "rust-ini"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091"
dependencies = [
"cfg-if",
"ordered-multimap",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
@ -725,12 +925,58 @@ dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "semver"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]]
name = "serde"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
]
[[package]]
name = "serde_json"
version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
dependencies = [
"serde",
]
[[package]]
name = "sha-1"
version = "0.10.1"
@ -742,6 +988,17 @@ dependencies = [
"digest",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -818,14 +1075,19 @@ dependencies = [
"anyhow",
"async-trait",
"bytes",
"config",
"ffmpeg-sys-next",
"futures-util",
"libc",
"log",
"pretty-hex",
"pretty_env_logger",
"serde",
"srt-tokio",
"tokio",
"tokio-stream",
"url",
"uuid",
]
[[package]]
@ -900,6 +1162,15 @@ dependencies = [
"syn 2.0.52",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
@ -968,12 +1239,52 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c12219811e0c1ba077867254e5ad62ee2c9c190b0d957110750ac0cda1ae96cd"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-trie"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
[[package]]
name = "unicode-bidi"
version = "0.3.15"
@ -995,6 +1306,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "url"
version = "2.5.0"
@ -1006,6 +1323,16 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "uuid"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
dependencies = [
"getrandom",
"serde",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
@ -1186,3 +1513,21 @@ name = "windows_x86_64_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]]
name = "winnow"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
dependencies = [
"memchr",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]

View File

@ -5,8 +5,8 @@ edition = "2021"
[dependencies]
srt-tokio = "0.4.3"
tokio = { version = "1.36.0" , features = ["rt-multi-thread"]}
anyhow = "1.0.80"
tokio = { version = "1.36.0" , features = ["rt-multi-thread", "sync"]}
anyhow = { version = "1.0.80", features = ["backtrace"] }
pretty_env_logger = "0.5.0"
bytes = "1.5.0"
tokio-stream = "0.1.14"
@ -15,3 +15,8 @@ async-trait = "0.1.77"
log = "0.4.21"
ffmpeg-sys-next = { version = "6.1.0", features = ["avformat", "avcodec", "swscale", "avfilter"]}
libc = "0.2.153"
pretty-hex = "0.4.1"
uuid = { version = "1.8.0", features = ["v4", "serde"] }
serde = { version = "1.0.197", features = ["derive"] }
config = { version = "0.14.0", features = ["toml"] }
url = "2.5.0"

4
Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM ubuntu:latest
LABEL authors="Kieran"
ENTRYPOINT ["top", "-b"]

12
config.toml Normal file
View File

@ -0,0 +1,12 @@
# List of endpoints to listen on
# currently supporting srt/tcp
endpoints = [
"srt://127.0.0.1:3333",
"tcp://127.0.0.1:3334"
]
# Output directory for egress
output_dir = "./out"
# Wehook system url (required)
webhook_url = "http://localhost:5873/api/v1/stream-core"

112
src/decode/mod.rs Normal file
View 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
View 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(())
}
}
}

View File

@ -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
View 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
View File

@ -0,0 +1 @@
pub mod hls;

185
src/encode/mod.rs Normal file
View 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
View 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,
}
}
}

View File

@ -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,
}

View File

@ -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
View 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
View 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()?)
}
}

View File

@ -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(())
}

View File

@ -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))
}
}

View File

@ -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>;
}

View File

@ -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
View 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
View 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
View 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
View 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
)
}
}

View File

@ -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 {}