feat: remux copy streams
Some checks failed
continuous-integration/drone/push Build is failing

closes #8
This commit is contained in:
2025-06-20 11:45:19 +01:00
parent e6bddcf641
commit add82b6933
6 changed files with 215 additions and 126 deletions

View File

@ -1,10 +1,9 @@
use anyhow::Result; use anyhow::Result;
use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVPacket; use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVPacket;
use ffmpeg_rs_raw::Encoder;
use std::path::PathBuf; use std::path::PathBuf;
use uuid::Uuid; use uuid::Uuid;
use crate::egress::{Egress, EgressResult}; use crate::egress::{Egress, EgressResult, EncoderOrSourceStream};
use crate::mux::{HlsMuxer, SegmentType}; use crate::mux::{HlsMuxer, SegmentType};
use crate::variant::VariantStream; use crate::variant::VariantStream;
@ -18,7 +17,7 @@ impl HlsEgress {
pub fn new<'a>( pub fn new<'a>(
out_dir: PathBuf, out_dir: PathBuf,
encoders: impl Iterator<Item = (&'a VariantStream, &'a Encoder)>, encoders: impl Iterator<Item = (&'a VariantStream, EncoderOrSourceStream<'a>)>,
segment_type: SegmentType, segment_type: SegmentType,
) -> Result<Self> { ) -> Result<Self> {
Ok(Self { Ok(Self {

View File

@ -1,5 +1,6 @@
use anyhow::Result; use anyhow::Result;
use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVPacket; use ffmpeg_rs_raw::ffmpeg_sys_the_third::{AVPacket, AVStream};
use ffmpeg_rs_raw::Encoder;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet; use std::collections::HashSet;
use std::path::PathBuf; use std::path::PathBuf;
@ -44,3 +45,8 @@ pub struct EgressSegment {
/// Path on disk to the segment file /// Path on disk to the segment file
pub path: PathBuf, pub path: PathBuf,
} }
pub enum EncoderOrSourceStream<'a> {
Encoder(&'a Encoder),
SourceStream(*mut AVStream),
}

View File

@ -1,13 +1,11 @@
use anyhow::Result; use anyhow::Result;
use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVPacket; use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVPacket;
use ffmpeg_rs_raw::{Encoder, Muxer}; use ffmpeg_rs_raw::Muxer;
use log::info;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use uuid::Uuid; use uuid::Uuid;
use crate::egress::{Egress, EgressResult}; use crate::egress::{Egress, EgressResult, EncoderOrSourceStream};
use crate::variant::{StreamMapping, VariantStream}; use crate::variant::{StreamMapping, VariantStream};
pub struct RecorderEgress { pub struct RecorderEgress {
@ -22,7 +20,7 @@ impl RecorderEgress {
pub fn new<'a>( pub fn new<'a>(
out_dir: PathBuf, out_dir: PathBuf,
variants: impl Iterator<Item = (&'a VariantStream, &'a Encoder)>, variants: impl Iterator<Item = (&'a VariantStream, EncoderOrSourceStream<'a>)>,
) -> Result<Self> { ) -> Result<Self> {
let out_file = out_dir.join(Self::FILENAME); let out_file = out_dir.join(Self::FILENAME);
let mut var_map = HashMap::new(); let mut var_map = HashMap::new();
@ -31,8 +29,16 @@ impl RecorderEgress {
.with_output_path(out_file.to_str().unwrap(), None)? .with_output_path(out_file.to_str().unwrap(), None)?
.build()?; .build()?;
for (var, enc) in variants { for (var, enc) in variants {
let stream = m.add_stream_encoder(enc)?; match enc {
var_map.insert(var.id(), (*stream).index); EncoderOrSourceStream::Encoder(enc) => {
let stream = m.add_stream_encoder(enc)?;
var_map.insert(var.id(), (*stream).index);
}
EncoderOrSourceStream::SourceStream(stream) => {
let stream = m.add_copy_stream(stream)?;
var_map.insert(var.id(), (*stream).index);
}
}
} }
let mut options = HashMap::new(); let mut options = HashMap::new();
options.insert("movflags".to_string(), "faststart".to_string()); options.insert("movflags".to_string(), "faststart".to_string());

View File

@ -1,4 +1,4 @@
use crate::egress::EgressResult; use crate::egress::{EgressResult, EncoderOrSourceStream};
use crate::mux::hls::variant::HlsVariant; use crate::mux::hls::variant::HlsVariant;
use crate::variant::{StreamMapping, VariantStream}; use crate::variant::{StreamMapping, VariantStream};
use anyhow::Result; use anyhow::Result;
@ -8,7 +8,9 @@ use itertools::Itertools;
use log::{trace, warn}; use log::{trace, warn};
use std::fmt::Display; use std::fmt::Display;
use std::fs::{remove_dir_all, File}; use std::fs::{remove_dir_all, File};
use std::ops::Sub;
use std::path::PathBuf; use std::path::PathBuf;
use tokio::time::Instant;
use uuid::Uuid; use uuid::Uuid;
mod segment; mod segment;
@ -69,14 +71,18 @@ pub enum SegmentType {
pub struct HlsMuxer { pub struct HlsMuxer {
pub out_dir: PathBuf, pub out_dir: PathBuf,
pub variants: Vec<HlsVariant>, pub variants: Vec<HlsVariant>,
last_master_write: Instant,
} }
impl HlsMuxer { impl HlsMuxer {
const MASTER_PLAYLIST: &'static str = "live.m3u8"; pub const MASTER_PLAYLIST: &'static str = "live.m3u8";
const MASTER_WRITE_INTERVAL: f32 = 60.0;
pub fn new<'a>( pub fn new<'a>(
out_dir: PathBuf, out_dir: PathBuf,
encoders: impl Iterator<Item = (&'a VariantStream, &'a Encoder)>, encoders: impl Iterator<Item = (&'a VariantStream, EncoderOrSourceStream<'a>)>,
segment_type: SegmentType, segment_type: SegmentType,
) -> Result<Self> { ) -> Result<Self> {
if !out_dir.exists() { if !out_dir.exists() {
@ -91,15 +97,16 @@ impl HlsMuxer {
vars.push(var); vars.push(var);
} }
let ret = Self { let mut ret = Self {
out_dir, out_dir,
variants: vars, variants: vars,
last_master_write: Instant::now(),
}; };
ret.write_master_playlist()?; ret.write_master_playlist()?;
Ok(ret) Ok(ret)
} }
fn write_master_playlist(&self) -> Result<()> { fn write_master_playlist(&mut self) -> Result<()> {
let mut pl = m3u8_rs::MasterPlaylist::default(); let mut pl = m3u8_rs::MasterPlaylist::default();
pl.version = Some(3); pl.version = Some(3);
pl.variants = self pl.variants = self
@ -110,6 +117,7 @@ impl HlsMuxer {
let mut f_out = File::create(self.out_dir.join(Self::MASTER_PLAYLIST))?; let mut f_out = File::create(self.out_dir.join(Self::MASTER_PLAYLIST))?;
pl.write_to(&mut f_out)?; pl.write_to(&mut f_out)?;
self.last_master_write = Instant::now();
Ok(()) Ok(())
} }
@ -119,6 +127,9 @@ impl HlsMuxer {
pkt: *mut AVPacket, pkt: *mut AVPacket,
variant: &Uuid, variant: &Uuid,
) -> Result<EgressResult> { ) -> Result<EgressResult> {
if Instant::now().sub(self.last_master_write).as_secs_f32() > Self::MASTER_WRITE_INTERVAL {
self.write_master_playlist()?;
}
for var in self.variants.iter_mut() { for var in self.variants.iter_mut() {
if let Some(vs) = var.streams.iter().find(|s| s.id() == variant) { if let Some(vs) = var.streams.iter().find(|s| s.id() == variant) {
// very important for muxer to know which stream this pkt belongs to // very important for muxer to know which stream this pkt belongs to
@ -140,7 +151,11 @@ impl HlsMuxer {
impl Drop for HlsMuxer { impl Drop for HlsMuxer {
fn drop(&mut self) { fn drop(&mut self) {
if let Err(e) = remove_dir_all(&self.out_dir) { if let Err(e) = remove_dir_all(&self.out_dir) {
warn!("Failed to clean up hls dir: {} {}", self.out_dir.display(), e); warn!(
"Failed to clean up hls dir: {} {}",
self.out_dir.display(),
e
);
} }
} }
} }

View File

@ -1,4 +1,4 @@
use crate::egress::{EgressResult, EgressSegment}; use crate::egress::{EgressResult, EgressSegment, EncoderOrSourceStream};
use crate::mux::hls::segment::{HlsSegment, PartialSegmentInfo, SegmentInfo}; use crate::mux::hls::segment::{HlsSegment, PartialSegmentInfo, SegmentInfo};
use crate::mux::{HlsVariantStream, SegmentType}; use crate::mux::{HlsVariantStream, SegmentType};
use crate::variant::{StreamMapping, VariantStream}; use crate::variant::{StreamMapping, VariantStream};
@ -6,14 +6,15 @@ use anyhow::{bail, ensure, Result};
use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVCodecID::AV_CODEC_ID_H264; use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVCodecID::AV_CODEC_ID_H264;
use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVMediaType::AVMEDIA_TYPE_VIDEO; use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVMediaType::AVMEDIA_TYPE_VIDEO;
use ffmpeg_rs_raw::ffmpeg_sys_the_third::{ use ffmpeg_rs_raw::ffmpeg_sys_the_third::{
av_free, av_q2d, av_write_frame, avio_close, avio_flush, avio_open, avio_size, AVPacket, av_free, av_get_bits_per_pixel, av_pix_fmt_desc_get, av_q2d, av_write_frame, avio_close,
AVIO_FLAG_WRITE, AV_NOPTS_VALUE, AV_PKT_FLAG_KEY, avio_flush, avio_open, avio_size, AVPacket, AVIO_FLAG_WRITE, AV_NOPTS_VALUE, AV_PKT_FLAG_KEY,
}; };
use ffmpeg_rs_raw::{cstr, Encoder, Muxer}; use ffmpeg_rs_raw::{cstr, Encoder, Muxer};
use log::{debug, info, trace, warn}; use log::{debug, info, trace, warn};
use m3u8_rs::{ExtTag, MediaSegmentType, PartInf, PreloadHint}; use m3u8_rs::{ExtTag, MediaSegmentType, PartInf, PreloadHint};
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, File};
use std::mem::transmute;
use std::path::PathBuf; use std::path::PathBuf;
use std::ptr; use std::ptr;
@ -60,7 +61,7 @@ impl HlsVariant {
pub fn new<'a>( pub fn new<'a>(
out_dir: PathBuf, out_dir: PathBuf,
group: usize, group: usize,
encoded_vars: impl Iterator<Item = (&'a VariantStream, &'a Encoder)>, encoded_vars: impl Iterator<Item = (&'a VariantStream, EncoderOrSourceStream<'a>)>,
segment_type: SegmentType, segment_type: SegmentType,
) -> Result<Self> { ) -> Result<Self> {
let name = format!("stream_{}", group); let name = format!("stream_{}", group);
@ -87,44 +88,71 @@ impl HlsVariant {
let mut segment_length = 1.0; let mut segment_length = 1.0;
for (var, enc) in encoded_vars { for (var, enc) in encoded_vars {
match var { match enc {
VariantStream::Video(v) => unsafe { EncoderOrSourceStream::Encoder(enc) => match var {
let stream = mux.add_stream_encoder(enc)?; VariantStream::Video(v) => unsafe {
let stream_idx = (*stream).index as usize; let stream = mux.add_stream_encoder(enc)?;
streams.push(HlsVariantStream::Video { let stream_idx = (*stream).index as usize;
group, streams.push(HlsVariantStream::Video {
index: stream_idx, group,
id: v.id(), index: stream_idx,
}); id: v.id(),
has_video = true; });
// Always use video stream as reference for segmentation has_video = true;
ref_stream_index = stream_idx as _;
let sg = v.keyframe_interval as f32 / v.fps;
if sg > segment_length {
segment_length = sg;
}
},
VariantStream::Audio(a) => unsafe {
let stream = mux.add_stream_encoder(enc)?;
let stream_idx = (*stream).index as usize;
streams.push(HlsVariantStream::Audio {
group,
index: stream_idx,
id: a.id(),
});
if !has_video && ref_stream_index == -1 {
ref_stream_index = stream_idx as _; ref_stream_index = stream_idx as _;
} let sg = v.keyframe_interval as f32 / v.fps;
if sg > segment_length {
segment_length = sg;
}
},
VariantStream::Audio(a) => unsafe {
let stream = mux.add_stream_encoder(enc)?;
let stream_idx = (*stream).index as usize;
streams.push(HlsVariantStream::Audio {
group,
index: stream_idx,
id: a.id(),
});
if !has_video && ref_stream_index == -1 {
ref_stream_index = stream_idx as _;
}
},
VariantStream::Subtitle(s) => unsafe {
let stream = mux.add_stream_encoder(enc)?;
streams.push(HlsVariantStream::Subtitle {
group,
index: (*stream).index as usize,
id: s.id(),
})
},
_ => bail!("unsupported variant stream"),
}, },
VariantStream::Subtitle(s) => unsafe { EncoderOrSourceStream::SourceStream(stream) => match var {
let stream = mux.add_stream_encoder(enc)?; VariantStream::CopyVideo(v) => unsafe {
streams.push(HlsVariantStream::Subtitle { let stream = mux.add_copy_stream(stream)?;
group, let stream_idx = (*stream).index as usize;
index: (*stream).index as usize, streams.push(HlsVariantStream::Video {
id: s.id(), group,
}) index: stream_idx,
id: v.id(),
});
has_video = true;
ref_stream_index = stream_idx as _;
},
VariantStream::CopyAudio(a) => unsafe {
let stream = mux.add_copy_stream(stream)?;
let stream_idx = (*stream).index as usize;
streams.push(HlsVariantStream::Audio {
group,
index: stream_idx,
id: a.id(),
});
if !has_video && ref_stream_index == -1 {
ref_stream_index = stream_idx as _;
}
},
_ => bail!("unsupported variant stream"),
}, },
_ => bail!("unsupported variant stream"),
} }
} }
ensure!( ensure!(
@ -597,17 +625,29 @@ impl HlsVariant {
let pes = self.video_stream().unwrap_or(self.streams.first().unwrap()); let pes = self.video_stream().unwrap_or(self.streams.first().unwrap());
let av_stream = *(*self.mux.context()).streams.add(*pes.index()); let av_stream = *(*self.mux.context()).streams.add(*pes.index());
let codec_par = (*av_stream).codecpar; let codec_par = (*av_stream).codecpar;
let bitrate = (*codec_par).bit_rate as u64;
let fps = av_q2d((*codec_par).framerate);
m3u8_rs::VariantStream { m3u8_rs::VariantStream {
is_i_frame: false, is_i_frame: false,
uri: format!("{}/live.m3u8", self.name), uri: format!("{}/live.m3u8", self.name),
bandwidth: (*codec_par).bit_rate as u64, bandwidth: if bitrate == 0 {
// make up bitrate when unknown (copy streams)
// this is the bitrate as a raw decoded stream, it's not accurate at all
// It only serves the purpose of ordering the copy streams as having the highest bitrate
let pix_desc = av_pix_fmt_desc_get(transmute((*codec_par).format));
(*codec_par).width as u64
* (*codec_par).height as u64
* av_get_bits_per_pixel(pix_desc) as u64
} else {
bitrate
},
average_bandwidth: None, average_bandwidth: None,
codecs: self.to_codec_attr(), codecs: self.to_codec_attr(),
resolution: Some(m3u8_rs::Resolution { resolution: Some(m3u8_rs::Resolution {
width: (*codec_par).width as _, width: (*codec_par).width as _,
height: (*codec_par).height as _, height: (*codec_par).height as _,
}), }),
frame_rate: Some(av_q2d((*codec_par).framerate)), frame_rate: if fps > 0.0 { Some(fps) } else { None },
hdcp_level: None, hdcp_level: None,
audio: None, audio: None,
video: None, video: None,

View File

@ -10,7 +10,7 @@ use std::time::{Duration, Instant};
use crate::egress::hls::HlsEgress; use crate::egress::hls::HlsEgress;
use crate::egress::recorder::RecorderEgress; use crate::egress::recorder::RecorderEgress;
use crate::egress::{Egress, EgressResult}; use crate::egress::{Egress, EgressResult, EncoderOrSourceStream};
use crate::generator::FrameGenerator; use crate::generator::FrameGenerator;
use crate::ingress::ConnectionInfo; use crate::ingress::ConnectionInfo;
use crate::mux::SegmentType; use crate::mux::SegmentType;
@ -101,9 +101,6 @@ pub struct PipelineRunner {
/// Encoder for a variant (variant_id, Encoder) /// Encoder for a variant (variant_id, Encoder)
encoders: HashMap<Uuid, Encoder>, encoders: HashMap<Uuid, Encoder>,
/// Simple mapping to copy streams
copy_stream: HashMap<Uuid, Uuid>,
/// All configured egress' /// All configured egress'
egress: Vec<Box<dyn Egress>>, egress: Vec<Box<dyn Egress>>,
@ -164,7 +161,6 @@ impl PipelineRunner {
scalers: Default::default(), scalers: Default::default(),
resampler: Default::default(), resampler: Default::default(),
encoders: Default::default(), encoders: Default::default(),
copy_stream: Default::default(),
fps_counter_start: Instant::now(), fps_counter_start: Instant::now(),
egress: Vec::new(), egress: Vec::new(),
frame_ctr: 0, frame_ctr: 0,
@ -366,51 +362,65 @@ impl PipelineRunner {
// Process all packets (original or converted) // Process all packets (original or converted)
let mut egress_results = vec![]; let mut egress_results = vec![];
// TODO: For copy streams, skip decoder // only process via decoder if there is more than 1 encoder
let frames = match self.decoder.decode_pkt(packet) { if !self.encoders.is_empty() {
Ok(f) => { let frames = match self.decoder.decode_pkt(packet) {
// Reset failure counter on successful decode Ok(f) => {
self.consecutive_decode_failures = 0; // Reset failure counter on successful decode
f self.consecutive_decode_failures = 0;
} f
Err(e) => {
self.consecutive_decode_failures += 1;
// Enhanced error logging with context
let packet_info = if !packet.is_null() {
format!(
"stream_idx={}, size={}, pts={}, dts={}",
(*packet).stream_index,
(*packet).size,
(*packet).pts,
(*packet).dts
)
} else {
"null packet".to_string()
};
warn!(
"Error decoding packet ({}): {}. Consecutive failures: {}/{}. Skipping packet.",
packet_info, e, self.consecutive_decode_failures, self.max_consecutive_failures
);
return self.handle_decode_failure(&config);
}
};
for (frame, stream_idx) in frames {
let stream = self.demuxer.get_stream(stream_idx as usize)?;
// Adjust frame pts time without start_offset
// Egress streams don't have a start time offset
if !stream.is_null() {
if (*stream).start_time != AV_NOPTS_VALUE {
(*frame).pts -= (*stream).start_time;
} }
(*frame).time_base = (*stream).time_base; Err(e) => {
} self.consecutive_decode_failures += 1;
let results = self.process_frame(&config, stream_idx as usize, frame)?; // Enhanced error logging with context
egress_results.extend(results); let packet_info = if !packet.is_null() {
format!(
"stream_idx={}, size={}, pts={}, dts={}",
(*packet).stream_index,
(*packet).size,
(*packet).pts,
(*packet).dts
)
} else {
"null packet".to_string()
};
warn!(
"Error decoding packet ({}): {}. Consecutive failures: {}/{}. Skipping packet.",
packet_info, e, self.consecutive_decode_failures, self.max_consecutive_failures
);
return self.handle_decode_failure(&config);
}
};
for (frame, stream_idx) in frames {
let stream = self.demuxer.get_stream(stream_idx as usize)?;
// Adjust frame pts time without start_offset
// Egress streams don't have a start time offset
if !stream.is_null() {
if (*stream).start_time != AV_NOPTS_VALUE {
(*frame).pts -= (*stream).start_time;
}
(*frame).time_base = (*stream).time_base;
}
let results = self.process_frame(&config, stream_idx as usize, frame)?;
egress_results.extend(results);
}
}
// egress (mux) copy variants
for var in config.variants {
match var {
VariantStream::CopyVideo(v) | VariantStream::CopyAudio(v)
if v.src_index == (*packet).stream_index as _ =>
{
egress_results.extend(Self::egress_packet(&mut self.egress, packet, &v.id())?);
}
_ => {}
}
} }
Ok(egress_results) Ok(egress_results)
@ -436,7 +446,6 @@ impl PipelineRunner {
let enc = if let Some(enc) = self.encoders.get_mut(&var.id()) { let enc = if let Some(enc) = self.encoders.get_mut(&var.id()) {
enc enc
} else { } else {
warn!("Frame had nowhere to go in {} :/", var.id());
continue; continue;
}; };
@ -512,7 +521,6 @@ impl PipelineRunner {
encoder: &mut Encoder, encoder: &mut Encoder,
frame: *mut AVFrame, frame: *mut AVFrame,
) -> Result<Vec<EgressResult>> { ) -> Result<Vec<EgressResult>> {
let mut ret = vec![];
// before encoding frame, rescale timestamps // before encoding frame, rescale timestamps
if !frame.is_null() { if !frame.is_null() {
let enc_ctx = encoder.codec_context(); let enc_ctx = encoder.codec_context();
@ -526,16 +534,25 @@ impl PipelineRunner {
} }
let packets = encoder.encode_frame(frame)?; let packets = encoder.encode_frame(frame)?;
// pass new packets to egress let mut ret = vec![];
for mut pkt in packets { for pkt in packets {
for eg in egress.iter_mut() { ret.extend(Self::egress_packet(egress, pkt, &var.id())?);
let pkt_clone = av_packet_clone(pkt);
let er = eg.process_pkt(pkt_clone, &var.id())?;
ret.push(er);
}
av_packet_free(&mut pkt);
} }
Ok(ret)
}
unsafe fn egress_packet(
egress: &mut Vec<Box<dyn Egress>>,
mut pkt: *mut AVPacket,
variant: &Uuid,
) -> Result<Vec<EgressResult>> {
let mut ret = vec![];
for eg in egress.iter_mut() {
let mut pkt_clone = av_packet_clone(pkt);
let er = eg.process_pkt(pkt_clone, variant)?;
av_packet_free(&mut pkt_clone);
ret.push(er);
}
Ok(ret) Ok(ret)
} }
@ -714,26 +731,33 @@ impl PipelineRunner {
} }
} }
// TODO: Setup copy streams
// Setup egress // Setup egress
for e in &cfg.egress { for e in &cfg.egress {
let c = e.config(); let c = e.config();
let encoders = self.encoders.iter().filter_map(|(k, v)| { let vars = c
if c.variants.contains(k) { .variants
let var = cfg.variants.iter().find(|x| x.id() == *k)?; .iter()
Some((var, v)) .map_while(|x| cfg.variants.iter().find(|z| z.id() == *x));
let variant_mapping = vars.map_while(|v| {
if let Some(e) = self.encoders.get(&v.id()) {
Some((v, EncoderOrSourceStream::Encoder(e)))
} else { } else {
None Some((
v,
EncoderOrSourceStream::SourceStream(unsafe {
self.demuxer.get_stream(v.src_index()).ok()?
}),
))
} }
}); });
match e { match e {
EgressType::HLS(_) => { EgressType::HLS(_) => {
let hls = HlsEgress::new(self.out_dir.clone(), encoders, SegmentType::MPEGTS)?; let hls =
HlsEgress::new(self.out_dir.clone(), variant_mapping, SegmentType::MPEGTS)?;
self.egress.push(Box::new(hls)); self.egress.push(Box::new(hls));
} }
EgressType::Recorder(_) => { EgressType::Recorder(_) => {
let rec = RecorderEgress::new(self.out_dir.clone(), encoders)?; let rec = RecorderEgress::new(self.out_dir.clone(), variant_mapping)?;
self.egress.push(Box::new(rec)); self.egress.push(Box::new(rec));
} }
_ => warn!("{} is not implemented", e), _ => warn!("{} is not implemented", e),
@ -756,7 +780,6 @@ impl Drop for PipelineRunner {
self.encoders.clear(); self.encoders.clear();
self.scalers.clear(); self.scalers.clear();
self.resampler.clear(); self.resampler.clear();
self.copy_stream.clear();
self.egress.clear(); self.egress.clear();
info!( info!(