mirror of
https://github.com/v0l/zap-stream-core.git
synced 2025-06-21 06:00:45 +00:00
Decoder validation
This commit is contained in:
@ -1,11 +1,16 @@
|
||||
use std::io::Read;
|
||||
use std::ptr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use bytes::Bytes;
|
||||
use ffmpeg_sys_next::*;
|
||||
use bytes::{BufMut, Bytes};
|
||||
use ffmpeg_sys_next::AVMediaType::{AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO};
|
||||
use ffmpeg_sys_next::*;
|
||||
use log::{info, warn};
|
||||
use tokio::sync::mpsc::error::TryRecvError;
|
||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::demux::info::{DemuxStreamInfo, StreamChannelType, StreamInfoChannel};
|
||||
@ -25,34 +30,53 @@ pub mod info;
|
||||
///
|
||||
pub(crate) struct Demuxer {
|
||||
ctx: *mut AVFormatContext,
|
||||
chan_in: UnboundedReceiver<Bytes>,
|
||||
chan_out: UnboundedSender<PipelinePayload>,
|
||||
started: Instant,
|
||||
state: DemuxerBuffer,
|
||||
}
|
||||
|
||||
unsafe impl Send for Demuxer {}
|
||||
|
||||
unsafe impl Sync for Demuxer {}
|
||||
|
||||
struct DemuxerBuffer {
|
||||
pub chan_in: UnboundedReceiver<Bytes>,
|
||||
pub buffer: bytes::BytesMut,
|
||||
}
|
||||
|
||||
unsafe extern "C" fn read_data(
|
||||
opaque: *mut libc::c_void,
|
||||
buffer: *mut libc::c_uchar,
|
||||
size: libc::c_int,
|
||||
) -> libc::c_int {
|
||||
let chan = opaque as *mut UnboundedReceiver<Bytes>;
|
||||
if let Some(data) = (*chan).blocking_recv() {
|
||||
let buff_len = data.len();
|
||||
assert!(size as usize >= buff_len);
|
||||
if buff_len > 0 {
|
||||
memcpy(
|
||||
buffer as *mut libc::c_void,
|
||||
data.as_ptr() as *const libc::c_void,
|
||||
buff_len as libc::c_ulonglong,
|
||||
);
|
||||
let state = opaque as *mut DemuxerBuffer;
|
||||
loop {
|
||||
match (*state).chan_in.try_recv() {
|
||||
Ok(data) => {
|
||||
if data.len() > 0 {
|
||||
(*state).buffer.put(data);
|
||||
}
|
||||
if (*state).buffer.len() >= size as usize {
|
||||
let buf_take = (*state).buffer.split_to(size as usize);
|
||||
memcpy(
|
||||
buffer as *mut libc::c_void,
|
||||
buf_take.as_ptr() as *const libc::c_void,
|
||||
buf_take.len() as libc::c_ulonglong,
|
||||
);
|
||||
return size;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
TryRecvError::Empty => {
|
||||
}
|
||||
TryRecvError::Disconnected => {
|
||||
warn!("EOF");
|
||||
return AVERROR_EOF;
|
||||
}
|
||||
},
|
||||
}
|
||||
buff_len as libc::c_int
|
||||
} else {
|
||||
AVERROR_EOF
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,18 +91,22 @@ impl Demuxer {
|
||||
|
||||
Self {
|
||||
ctx: ps,
|
||||
chan_in,
|
||||
chan_out,
|
||||
state: DemuxerBuffer {
|
||||
chan_in,
|
||||
buffer: bytes::BytesMut::new(),
|
||||
},
|
||||
started: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn probe_input(&mut self) -> Result<DemuxStreamInfo, Error> {
|
||||
let buf_ptr = ptr::from_mut(&mut self.chan_in) as *mut libc::c_void;
|
||||
const BUFFER_SIZE: usize = 4096;
|
||||
let buf_ptr = ptr::from_mut(&mut self.state) as *mut libc::c_void;
|
||||
let pb = avio_alloc_context(
|
||||
av_mallocz(4096) as *mut libc::c_uchar,
|
||||
4096,
|
||||
av_mallocz(BUFFER_SIZE) as *mut libc::c_uchar,
|
||||
BUFFER_SIZE as libc::c_int,
|
||||
0,
|
||||
buf_ptr,
|
||||
Some(read_data),
|
||||
|
@ -3,7 +3,11 @@ use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
|
||||
use anyhow::Error;
|
||||
use ffmpeg_sys_next::{av_dump_format, av_guess_format, av_interleaved_write_frame, av_strdup, avformat_alloc_context, avformat_alloc_output_context2, avformat_free_context, avformat_write_header, AVFormatContext, AVIO_FLAG_READ_WRITE, avio_flush, avio_open2, AVPacket};
|
||||
use ffmpeg_sys_next::{
|
||||
av_dump_format, av_guess_format, av_interleaved_write_frame, av_strdup, avformat_alloc_context
|
||||
, avformat_free_context,
|
||||
AVFormatContext, AVIO_FLAG_READ_WRITE, avio_open2, AVPacket,
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -54,11 +58,11 @@ impl RecorderEgress {
|
||||
}
|
||||
let base = format!("{}/{}", self.config.out_dir, self.id);
|
||||
|
||||
let out_file = format!("{}/recording.mkv\0", base).as_ptr() as *const libc::c_char;
|
||||
let out_file = format!("{}/recording.mkv\0", base);
|
||||
fs::create_dir_all(base.clone())?;
|
||||
let ret = avio_open2(
|
||||
&mut (*ctx).pb,
|
||||
out_file,
|
||||
out_file.as_ptr() as *const libc::c_char,
|
||||
AVIO_FLAG_READ_WRITE,
|
||||
ptr::null(),
|
||||
ptr::null_mut(),
|
||||
@ -68,16 +72,16 @@ impl RecorderEgress {
|
||||
}
|
||||
(*ctx).oformat = av_guess_format(
|
||||
"matroska\0".as_ptr() as *const libc::c_char,
|
||||
out_file,
|
||||
out_file.as_ptr() as *const libc::c_char,
|
||||
ptr::null(),
|
||||
);
|
||||
if (*ctx).oformat.is_null() {
|
||||
return Err(Error::msg("Output format not found"));
|
||||
}
|
||||
(*ctx).url = av_strdup(out_file);
|
||||
(*ctx).url = av_strdup(out_file.as_ptr() as *const libc::c_char);
|
||||
map_variants_to_streams(ctx, &mut self.config.variants)?;
|
||||
|
||||
let ret = avformat_write_header(ctx, ptr::null_mut());
|
||||
//let ret = avformat_write_header(ctx, ptr::null_mut());
|
||||
if ret < 0 {
|
||||
return Err(Error::msg(get_ffmpeg_error_msg(ret)));
|
||||
}
|
||||
|
70
src/ingress/file.rs
Normal file
70
src/ingress/file.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use std::{ptr, slice};
|
||||
use std::mem::transmute;
|
||||
use std::ops::Add;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use bytes::BufMut;
|
||||
use ffmpeg_sys_next::{
|
||||
av_frame_alloc, av_frame_copy_props, av_frame_free, av_frame_get_buffer, av_packet_alloc,
|
||||
av_packet_free, AV_PROFILE_AV1_HIGH, AV_PROFILE_H264_HIGH, av_q2d, av_write_frame,
|
||||
avcodec_alloc_context3, avcodec_find_encoder, avcodec_get_name, avcodec_open2,
|
||||
avcodec_receive_packet, avcodec_send_frame, AVERROR, avformat_alloc_context,
|
||||
avformat_alloc_output_context2, AVRational, EAGAIN, sws_alloc_context, SWS_BILINEAR, sws_getContext,
|
||||
sws_scale_frame,
|
||||
};
|
||||
use ffmpeg_sys_next::AVCodecID::{
|
||||
AV_CODEC_ID_H264, AV_CODEC_ID_MPEG1VIDEO, AV_CODEC_ID_MPEG2VIDEO, AV_CODEC_ID_MPEG4,
|
||||
AV_CODEC_ID_VP8, AV_CODEC_ID_WMV1,
|
||||
};
|
||||
use ffmpeg_sys_next::AVColorSpace::AVCOL_SPC_RGB;
|
||||
use ffmpeg_sys_next::AVPictureType::{AV_PICTURE_TYPE_I, AV_PICTURE_TYPE_NONE};
|
||||
use ffmpeg_sys_next::AVPixelFormat::{AV_PIX_FMT_RGB24, AV_PIX_FMT_YUV420P};
|
||||
use futures_util::StreamExt;
|
||||
use libc::memcpy;
|
||||
use log::{error, info, warn};
|
||||
use rand::random;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
use crate::ingress::ConnectionInfo;
|
||||
use crate::pipeline::builder::PipelineBuilder;
|
||||
|
||||
pub async fn listen(path: PathBuf, builder: PipelineBuilder) -> Result<(), anyhow::Error> {
|
||||
info!("Sending file {}", path.to_str().unwrap());
|
||||
|
||||
tokio::spawn(async move {
|
||||
let (tx, rx) = unbounded_channel();
|
||||
let info = ConnectionInfo {
|
||||
ip_addr: "".to_owned(),
|
||||
endpoint: "file-input".to_owned(),
|
||||
};
|
||||
|
||||
if let Ok(mut pl) = builder.build_for(info, rx).await {
|
||||
std::thread::spawn(move || loop {
|
||||
if let Err(e) = pl.run() {
|
||||
warn!("Pipeline error: {}", e.backtrace());
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if let Ok(mut stream) = tokio::fs::File::open(path).await {
|
||||
let mut buf = [0u8; 1500];
|
||||
loop {
|
||||
if let Ok(r) = stream.read(&mut buf).await {
|
||||
if r > 0 {
|
||||
tx.send(bytes::Bytes::copy_from_slice(&buf[..r])).unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
info!("EOF");
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod srt;
|
||||
pub mod tcp;
|
||||
pub mod test;
|
||||
pub mod file;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ConnectionInfo {
|
||||
|
158
src/ingress/test.rs
Normal file
158
src/ingress/test.rs
Normal file
@ -0,0 +1,158 @@
|
||||
use std::{ptr, slice};
|
||||
use std::mem::transmute;
|
||||
use std::ops::Add;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use bytes::BufMut;
|
||||
use ffmpeg_sys_next::{
|
||||
av_frame_alloc, av_frame_copy_props, av_frame_free, av_frame_get_buffer, av_packet_alloc,
|
||||
av_packet_free, AV_PROFILE_AV1_HIGH, AV_PROFILE_H264_HIGH, AV_PROFILE_H264_MAIN, av_q2d,
|
||||
av_write_frame, avcodec_alloc_context3, avcodec_find_encoder, avcodec_get_name,
|
||||
avcodec_open2, avcodec_receive_packet, avcodec_send_frame, AVERROR,
|
||||
avformat_alloc_context, avformat_alloc_output_context2, AVRational, EAGAIN, sws_alloc_context,
|
||||
SWS_BILINEAR, sws_getContext, sws_scale_frame,
|
||||
};
|
||||
use ffmpeg_sys_next::AVCodecID::{
|
||||
AV_CODEC_ID_H264, AV_CODEC_ID_MPEG1VIDEO, AV_CODEC_ID_MPEG2VIDEO, AV_CODEC_ID_MPEG4,
|
||||
AV_CODEC_ID_VP8, AV_CODEC_ID_WMV1,
|
||||
};
|
||||
use ffmpeg_sys_next::AVColorSpace::{AVCOL_SPC_BT709, AVCOL_SPC_RGB};
|
||||
use ffmpeg_sys_next::AVPictureType::{AV_PICTURE_TYPE_I, AV_PICTURE_TYPE_NONE};
|
||||
use ffmpeg_sys_next::AVPixelFormat::{AV_PIX_FMT_RGB24, AV_PIX_FMT_YUV420P};
|
||||
use futures_util::StreamExt;
|
||||
use libc::memcpy;
|
||||
use log::{error, info, warn};
|
||||
use rand::random;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
use crate::ingress::ConnectionInfo;
|
||||
use crate::pipeline::builder::PipelineBuilder;
|
||||
|
||||
pub async fn listen(builder: PipelineBuilder) -> Result<(), anyhow::Error> {
|
||||
info!("Test pattern enabled");
|
||||
|
||||
const WIDTH: libc::c_int = 1280;
|
||||
const HEIGHT: libc::c_int = 720;
|
||||
const TBN: libc::c_int = 30;
|
||||
|
||||
tokio::spawn(async move {
|
||||
let (tx, rx) = unbounded_channel();
|
||||
let info = ConnectionInfo {
|
||||
ip_addr: "".to_owned(),
|
||||
endpoint: "test-pattern".to_owned(),
|
||||
};
|
||||
|
||||
if let Ok(mut pl) = builder.build_for(info, rx).await {
|
||||
std::thread::spawn(move || loop {
|
||||
if let Err(e) = pl.run() {
|
||||
warn!("Pipeline error: {}", e.backtrace());
|
||||
break;
|
||||
}
|
||||
});
|
||||
unsafe {
|
||||
let codec = avcodec_find_encoder(AV_CODEC_ID_H264);
|
||||
let enc_ctx = avcodec_alloc_context3(codec);
|
||||
(*enc_ctx).width = WIDTH;
|
||||
(*enc_ctx).height = HEIGHT;
|
||||
(*enc_ctx).pix_fmt = AV_PIX_FMT_YUV420P;
|
||||
(*enc_ctx).colorspace = AVCOL_SPC_BT709;
|
||||
(*enc_ctx).bit_rate = 1_000_000;
|
||||
(*enc_ctx).framerate = AVRational { num: 30, den: 1 };
|
||||
(*enc_ctx).gop_size = 30;
|
||||
(*enc_ctx).level = 40;
|
||||
(*enc_ctx).profile = AV_PROFILE_H264_MAIN;
|
||||
(*enc_ctx).time_base = AVRational { num: 1, den: TBN };
|
||||
(*enc_ctx).pkt_timebase = (*enc_ctx).time_base;
|
||||
|
||||
avcodec_open2(enc_ctx, codec, ptr::null_mut());
|
||||
|
||||
let src_frame = av_frame_alloc();
|
||||
(*src_frame).width = WIDTH;
|
||||
(*src_frame).height = HEIGHT;
|
||||
(*src_frame).pict_type = AV_PICTURE_TYPE_NONE;
|
||||
(*src_frame).key_frame = 1;
|
||||
(*src_frame).colorspace = AVCOL_SPC_RGB;
|
||||
(*src_frame).format = AV_PIX_FMT_RGB24 as libc::c_int;
|
||||
(*src_frame).time_base = (*enc_ctx).time_base;
|
||||
av_frame_get_buffer(src_frame, 0);
|
||||
|
||||
let sws = sws_getContext(
|
||||
WIDTH as libc::c_int,
|
||||
HEIGHT as libc::c_int,
|
||||
transmute((*src_frame).format),
|
||||
WIDTH as libc::c_int,
|
||||
HEIGHT as libc::c_int,
|
||||
(*enc_ctx).pix_fmt,
|
||||
SWS_BILINEAR,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
);
|
||||
let svg_data = std::fs::read("./test.svg").unwrap();
|
||||
let tree = usvg::Tree::from_data(&svg_data, &Default::default()).unwrap();
|
||||
let mut pixmap = tiny_skia::Pixmap::new(WIDTH as u32, HEIGHT as u32).unwrap();
|
||||
let render_ts = tiny_skia::Transform::from_scale(0.5, 0.5);
|
||||
resvg::render(&tree, render_ts, &mut pixmap.as_mut());
|
||||
|
||||
for x in 0..WIDTH as u32 {
|
||||
for y in 0..HEIGHT as u32 {
|
||||
if let Some(px) = pixmap.pixel(x, y) {
|
||||
let offset = 3 * x + y * (*src_frame).linesize[0] as u32;
|
||||
let pixel = (*src_frame).data[0].add(offset as usize);
|
||||
*pixel.offset(0) = px.red();
|
||||
*pixel.offset(1) = px.green();
|
||||
*pixel.offset(2) = px.blue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut frame_number: u64 = 0;
|
||||
let start = SystemTime::now();
|
||||
loop {
|
||||
frame_number += 1;
|
||||
(*src_frame).pts = (TBN as u64 * frame_number) as i64;
|
||||
|
||||
let mut dst_frame = av_frame_alloc();
|
||||
av_frame_copy_props(dst_frame, src_frame);
|
||||
sws_scale_frame(sws, dst_frame, src_frame);
|
||||
|
||||
// encode
|
||||
let mut ret = avcodec_send_frame(enc_ctx, dst_frame);
|
||||
av_frame_free(&mut dst_frame);
|
||||
|
||||
while ret > 0 || ret == AVERROR(libc::EAGAIN) {
|
||||
let mut av_pkt = av_packet_alloc();
|
||||
ret = avcodec_receive_packet(enc_ctx, av_pkt);
|
||||
if ret != 0 {
|
||||
if ret == AVERROR(EAGAIN) {
|
||||
av_packet_free(&mut av_pkt);
|
||||
break;
|
||||
}
|
||||
error!("Encoder failed: {}", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
let buf = bytes::Bytes::from(slice::from_raw_parts(
|
||||
(*av_pkt).data,
|
||||
(*av_pkt).size as usize,
|
||||
));
|
||||
for z in 0..(buf.len() as f32 / 1024.0).ceil() as usize {
|
||||
if let Err(e) = tx.send(buf.slice(z..(z + 1024).min(buf.len()))) {
|
||||
error!("Failed to write data {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stream_time = Duration::from_secs_f64(
|
||||
frame_number as libc::c_double * av_q2d((*enc_ctx).time_base),
|
||||
);
|
||||
let real_time = SystemTime::now().duration_since(start).unwrap();
|
||||
let wait_time = stream_time - real_time;
|
||||
std::thread::sleep(wait_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
@ -64,6 +64,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
"0.0.0.0:8080".to_owned(),
|
||||
settings.clone(),
|
||||
)));
|
||||
listeners.push(tokio::spawn(ingress::file::listen(
|
||||
"/home/kieran/high_flight.mp4".parse().unwrap(),
|
||||
builder.clone(),
|
||||
)));
|
||||
//listeners.push(tokio::spawn(ingress::test::listen(builder.clone())));
|
||||
|
||||
for handle in listeners {
|
||||
if let Err(e) = handle.await {
|
||||
|
@ -41,19 +41,6 @@ impl Webhook {
|
||||
level: 51,
|
||||
keyframe_interval: 2,
|
||||
}));
|
||||
vars.push(VariantStream::Video(VideoVariant {
|
||||
id: Uuid::new_v4(),
|
||||
src_index: video_src.index,
|
||||
dst_index: 1,
|
||||
width: 640,
|
||||
height: 360,
|
||||
fps: video_src.fps as u16,
|
||||
bitrate: 1_000_000,
|
||||
codec: 27,
|
||||
profile: 100,
|
||||
level: 51,
|
||||
keyframe_interval: 2,
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(audio_src) = stream_info
|
||||
@ -71,32 +58,22 @@ impl Webhook {
|
||||
sample_rate: 48_000,
|
||||
sample_fmt: "s16".to_owned(),
|
||||
}));
|
||||
vars.push(VariantStream::Audio(AudioVariant {
|
||||
id: Uuid::new_v4(),
|
||||
src_index: audio_src.index,
|
||||
dst_index: 1,
|
||||
bitrate: 220_000,
|
||||
codec: 86018,
|
||||
channels: 2,
|
||||
sample_rate: 48_000,
|
||||
sample_fmt: "s16".to_owned(),
|
||||
}));
|
||||
}
|
||||
|
||||
PipelineConfig {
|
||||
id: Uuid::new_v4(),
|
||||
recording: vec![],
|
||||
egress: vec![
|
||||
EgressType::Recorder(EgressConfig {
|
||||
name: "Recorder".to_owned(),
|
||||
out_dir: self.config.output_dir.clone(),
|
||||
variants: vars.clone(),
|
||||
}),
|
||||
/*EgressType::MPEGTS(EgressConfig {
|
||||
name: "MPEGTS".to_owned(),
|
||||
/*EgressType::Recorder(EgressConfig {
|
||||
name: "REC".to_owned(),
|
||||
out_dir: self.config.output_dir.clone(),
|
||||
variants: vars.clone(),
|
||||
}),*/
|
||||
EgressType::HLS(EgressConfig {
|
||||
name: "HLS".to_owned(),
|
||||
out_dir: self.config.output_dir.clone(),
|
||||
variants: vars.clone(),
|
||||
}),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user