|
@@ -25,22 +25,28 @@ pub struct RecorderContext {
|
|
|
pub server: bool,
|
|
|
pub id: String,
|
|
|
pub dir: String,
|
|
|
+ pub display: usize,
|
|
|
+ pub tx: Option<Sender<RecordState>>,
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Debug, Clone)]
|
|
|
+pub struct RecorderContext2 {
|
|
|
pub filename: String,
|
|
|
pub width: usize,
|
|
|
pub height: usize,
|
|
|
pub format: CodecFormat,
|
|
|
- pub tx: Option<Sender<RecordState>>,
|
|
|
}
|
|
|
|
|
|
-impl RecorderContext {
|
|
|
- pub fn set_filename(&mut self) -> ResultType<()> {
|
|
|
- if !PathBuf::from(&self.dir).exists() {
|
|
|
- std::fs::create_dir_all(&self.dir)?;
|
|
|
+impl RecorderContext2 {
|
|
|
+ pub fn set_filename(&mut self, ctx: &RecorderContext) -> ResultType<()> {
|
|
|
+ if !PathBuf::from(&ctx.dir).exists() {
|
|
|
+ std::fs::create_dir_all(&ctx.dir)?;
|
|
|
}
|
|
|
- let file = if self.server { "incoming" } else { "outgoing" }.to_string()
|
|
|
+ let file = if ctx.server { "incoming" } else { "outgoing" }.to_string()
|
|
|
+ "_"
|
|
|
- + &self.id.clone()
|
|
|
+ + &ctx.id.clone()
|
|
|
+ &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string()
|
|
|
+ + &format!("display{}_", ctx.display)
|
|
|
+ &self.format.to_string().to_lowercase()
|
|
|
+ if self.format == CodecFormat::VP9
|
|
|
|| self.format == CodecFormat::VP8
|
|
@@ -50,11 +56,10 @@ impl RecorderContext {
|
|
|
} else {
|
|
|
".mp4"
|
|
|
};
|
|
|
- self.filename = PathBuf::from(&self.dir)
|
|
|
+ self.filename = PathBuf::from(&ctx.dir)
|
|
|
.join(file)
|
|
|
.to_string_lossy()
|
|
|
.to_string();
|
|
|
- log::info!("video will save to {}", self.filename);
|
|
|
Ok(())
|
|
|
}
|
|
|
}
|
|
@@ -63,7 +68,7 @@ unsafe impl Send for Recorder {}
|
|
|
unsafe impl Sync for Recorder {}
|
|
|
|
|
|
pub trait RecorderApi {
|
|
|
- fn new(ctx: RecorderContext) -> ResultType<Self>
|
|
|
+ fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self>
|
|
|
where
|
|
|
Self: Sized;
|
|
|
fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool;
|
|
@@ -78,13 +83,15 @@ pub enum RecordState {
|
|
|
}
|
|
|
|
|
|
pub struct Recorder {
|
|
|
- pub inner: Box<dyn RecorderApi>,
|
|
|
+ pub inner: Option<Box<dyn RecorderApi>>,
|
|
|
ctx: RecorderContext,
|
|
|
+ ctx2: Option<RecorderContext2>,
|
|
|
pts: Option<i64>,
|
|
|
+ check_failed: bool,
|
|
|
}
|
|
|
|
|
|
impl Deref for Recorder {
|
|
|
- type Target = Box<dyn RecorderApi>;
|
|
|
+ type Target = Option<Box<dyn RecorderApi>>;
|
|
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
&self.inner
|
|
@@ -98,114 +105,123 @@ impl DerefMut for Recorder {
|
|
|
}
|
|
|
|
|
|
impl Recorder {
|
|
|
- pub fn new(mut ctx: RecorderContext) -> ResultType<Self> {
|
|
|
- ctx.set_filename()?;
|
|
|
- let recorder = match ctx.format {
|
|
|
- CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Recorder {
|
|
|
- inner: Box::new(WebmRecorder::new(ctx.clone())?),
|
|
|
- ctx,
|
|
|
- pts: None,
|
|
|
- },
|
|
|
- #[cfg(feature = "hwcodec")]
|
|
|
- _ => Recorder {
|
|
|
- inner: Box::new(HwRecorder::new(ctx.clone())?),
|
|
|
- ctx,
|
|
|
- pts: None,
|
|
|
- },
|
|
|
- #[cfg(not(feature = "hwcodec"))]
|
|
|
- _ => bail!("unsupported codec type"),
|
|
|
- };
|
|
|
- recorder.send_state(RecordState::NewFile(recorder.ctx.filename.clone()));
|
|
|
- Ok(recorder)
|
|
|
+ pub fn new(ctx: RecorderContext) -> ResultType<Self> {
|
|
|
+ Ok(Self {
|
|
|
+ inner: None,
|
|
|
+ ctx,
|
|
|
+ ctx2: None,
|
|
|
+ pts: None,
|
|
|
+ check_failed: false,
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> {
|
|
|
- ctx.set_filename()?;
|
|
|
- self.inner = match ctx.format {
|
|
|
- CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => {
|
|
|
- Box::new(WebmRecorder::new(ctx.clone())?)
|
|
|
+ fn check(&mut self, w: usize, h: usize, format: CodecFormat) -> ResultType<()> {
|
|
|
+ match self.ctx2 {
|
|
|
+ Some(ref ctx2) => {
|
|
|
+ if ctx2.width != w || ctx2.height != h || ctx2.format != format {
|
|
|
+ let mut ctx2 = RecorderContext2 {
|
|
|
+ width: w,
|
|
|
+ height: h,
|
|
|
+ format,
|
|
|
+ filename: Default::default(),
|
|
|
+ };
|
|
|
+ ctx2.set_filename(&self.ctx)?;
|
|
|
+ self.ctx2 = Some(ctx2);
|
|
|
+ self.inner = None;
|
|
|
+ }
|
|
|
}
|
|
|
- #[cfg(feature = "hwcodec")]
|
|
|
- _ => Box::new(HwRecorder::new(ctx.clone())?),
|
|
|
- #[cfg(not(feature = "hwcodec"))]
|
|
|
- _ => bail!("unsupported codec type"),
|
|
|
+ None => {
|
|
|
+ let mut ctx2 = RecorderContext2 {
|
|
|
+ width: w,
|
|
|
+ height: h,
|
|
|
+ format,
|
|
|
+ filename: Default::default(),
|
|
|
+ };
|
|
|
+ ctx2.set_filename(&self.ctx)?;
|
|
|
+ self.ctx2 = Some(ctx2);
|
|
|
+ self.inner = None;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ let Some(ctx2) = &self.ctx2 else {
|
|
|
+ bail!("ctx2 is None");
|
|
|
};
|
|
|
- self.ctx = ctx;
|
|
|
- self.pts = None;
|
|
|
- self.send_state(RecordState::NewFile(self.ctx.filename.clone()));
|
|
|
+ if self.inner.is_none() {
|
|
|
+ self.inner = match format {
|
|
|
+ CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Some(Box::new(
|
|
|
+ WebmRecorder::new(self.ctx.clone(), (*ctx2).clone())?,
|
|
|
+ )),
|
|
|
+ #[cfg(feature = "hwcodec")]
|
|
|
+ _ => Some(Box::new(HwRecorder::new(
|
|
|
+ self.ctx.clone(),
|
|
|
+ (*ctx2).clone(),
|
|
|
+ )?)),
|
|
|
+ #[cfg(not(feature = "hwcodec"))]
|
|
|
+ _ => bail!("unsupported codec type"),
|
|
|
+ };
|
|
|
+ self.pts = None;
|
|
|
+ self.send_state(RecordState::NewFile(ctx2.filename.clone()));
|
|
|
+ }
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
- pub fn write_message(&mut self, msg: &Message) {
|
|
|
+ pub fn write_message(&mut self, msg: &Message, w: usize, h: usize) {
|
|
|
if let Some(message::Union::VideoFrame(vf)) = &msg.union {
|
|
|
if let Some(frame) = &vf.union {
|
|
|
- self.write_frame(frame).ok();
|
|
|
+ self.write_frame(frame, w, h).ok();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> {
|
|
|
+ pub fn write_frame(
|
|
|
+ &mut self,
|
|
|
+ frame: &video_frame::Union,
|
|
|
+ w: usize,
|
|
|
+ h: usize,
|
|
|
+ ) -> ResultType<()> {
|
|
|
+ if self.check_failed {
|
|
|
+ bail!("check failed");
|
|
|
+ }
|
|
|
+ let format = CodecFormat::from(frame);
|
|
|
+ if format == CodecFormat::Unknown {
|
|
|
+ bail!("unsupported frame type");
|
|
|
+ }
|
|
|
+ let res = self.check(w, h, format);
|
|
|
+ if res.is_err() {
|
|
|
+ self.check_failed = true;
|
|
|
+ log::error!("check failed: {:?}", res);
|
|
|
+ res?;
|
|
|
+ }
|
|
|
match frame {
|
|
|
video_frame::Union::Vp8s(vp8s) => {
|
|
|
- if self.ctx.format != CodecFormat::VP8 {
|
|
|
- self.change(RecorderContext {
|
|
|
- format: CodecFormat::VP8,
|
|
|
- ..self.ctx.clone()
|
|
|
- })?;
|
|
|
- }
|
|
|
for f in vp8s.frames.iter() {
|
|
|
- self.check_pts(f.pts)?;
|
|
|
- self.write_video(f);
|
|
|
+ self.check_pts(f.pts, w, h, format)?;
|
|
|
+ self.as_mut().map(|x| x.write_video(f));
|
|
|
}
|
|
|
}
|
|
|
video_frame::Union::Vp9s(vp9s) => {
|
|
|
- if self.ctx.format != CodecFormat::VP9 {
|
|
|
- self.change(RecorderContext {
|
|
|
- format: CodecFormat::VP9,
|
|
|
- ..self.ctx.clone()
|
|
|
- })?;
|
|
|
- }
|
|
|
for f in vp9s.frames.iter() {
|
|
|
- self.check_pts(f.pts)?;
|
|
|
- self.write_video(f);
|
|
|
+ self.check_pts(f.pts, w, h, format)?;
|
|
|
+ self.as_mut().map(|x| x.write_video(f));
|
|
|
}
|
|
|
}
|
|
|
video_frame::Union::Av1s(av1s) => {
|
|
|
- if self.ctx.format != CodecFormat::AV1 {
|
|
|
- self.change(RecorderContext {
|
|
|
- format: CodecFormat::AV1,
|
|
|
- ..self.ctx.clone()
|
|
|
- })?;
|
|
|
- }
|
|
|
for f in av1s.frames.iter() {
|
|
|
- self.check_pts(f.pts)?;
|
|
|
- self.write_video(f);
|
|
|
+ self.check_pts(f.pts, w, h, format)?;
|
|
|
+ self.as_mut().map(|x| x.write_video(f));
|
|
|
}
|
|
|
}
|
|
|
#[cfg(feature = "hwcodec")]
|
|
|
video_frame::Union::H264s(h264s) => {
|
|
|
- if self.ctx.format != CodecFormat::H264 {
|
|
|
- self.change(RecorderContext {
|
|
|
- format: CodecFormat::H264,
|
|
|
- ..self.ctx.clone()
|
|
|
- })?;
|
|
|
- }
|
|
|
for f in h264s.frames.iter() {
|
|
|
- self.check_pts(f.pts)?;
|
|
|
- self.write_video(f);
|
|
|
+ self.check_pts(f.pts, w, h, format)?;
|
|
|
+ self.as_mut().map(|x| x.write_video(f));
|
|
|
}
|
|
|
}
|
|
|
#[cfg(feature = "hwcodec")]
|
|
|
video_frame::Union::H265s(h265s) => {
|
|
|
- if self.ctx.format != CodecFormat::H265 {
|
|
|
- self.change(RecorderContext {
|
|
|
- format: CodecFormat::H265,
|
|
|
- ..self.ctx.clone()
|
|
|
- })?;
|
|
|
- }
|
|
|
for f in h265s.frames.iter() {
|
|
|
- self.check_pts(f.pts)?;
|
|
|
- self.write_video(f);
|
|
|
+ self.check_pts(f.pts, w, h, format)?;
|
|
|
+ self.as_mut().map(|x| x.write_video(f));
|
|
|
}
|
|
|
}
|
|
|
_ => bail!("unsupported frame type"),
|
|
@@ -214,13 +230,21 @@ impl Recorder {
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
- fn check_pts(&mut self, pts: i64) -> ResultType<()> {
|
|
|
+ fn check_pts(&mut self, pts: i64, w: usize, h: usize, format: CodecFormat) -> ResultType<()> {
|
|
|
// https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c
|
|
|
let old_pts = self.pts;
|
|
|
self.pts = Some(pts);
|
|
|
if old_pts.clone().unwrap_or_default() > pts {
|
|
|
log::info!("pts {:?} -> {}, change record filename", old_pts, pts);
|
|
|
- self.change(self.ctx.clone())?;
|
|
|
+ self.inner = None;
|
|
|
+ self.ctx2 = None;
|
|
|
+ let res = self.check(w, h, format);
|
|
|
+ if res.is_err() {
|
|
|
+ self.check_failed = true;
|
|
|
+ log::error!("check failed: {:?}", res);
|
|
|
+ res?;
|
|
|
+ }
|
|
|
+ self.pts = Some(pts);
|
|
|
}
|
|
|
Ok(())
|
|
|
}
|
|
@@ -234,21 +258,22 @@ struct WebmRecorder {
|
|
|
vt: VideoTrack,
|
|
|
webm: Option<Segment<Writer<File>>>,
|
|
|
ctx: RecorderContext,
|
|
|
+ ctx2: RecorderContext2,
|
|
|
key: bool,
|
|
|
written: bool,
|
|
|
start: Instant,
|
|
|
}
|
|
|
|
|
|
impl RecorderApi for WebmRecorder {
|
|
|
- fn new(ctx: RecorderContext) -> ResultType<Self> {
|
|
|
+ fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self> {
|
|
|
let out = match {
|
|
|
OpenOptions::new()
|
|
|
.write(true)
|
|
|
.create_new(true)
|
|
|
- .open(&ctx.filename)
|
|
|
+ .open(&ctx2.filename)
|
|
|
} {
|
|
|
Ok(file) => file,
|
|
|
- Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx.filename)?,
|
|
|
+ Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx2.filename)?,
|
|
|
Err(e) => return Err(e.into()),
|
|
|
};
|
|
|
let mut webm = match mux::Segment::new(mux::Writer::new(out)) {
|
|
@@ -256,18 +281,18 @@ impl RecorderApi for WebmRecorder {
|
|
|
None => bail!("Failed to create webm mux"),
|
|
|
};
|
|
|
let vt = webm.add_video_track(
|
|
|
- ctx.width as _,
|
|
|
- ctx.height as _,
|
|
|
+ ctx2.width as _,
|
|
|
+ ctx2.height as _,
|
|
|
None,
|
|
|
- if ctx.format == CodecFormat::VP9 {
|
|
|
+ if ctx2.format == CodecFormat::VP9 {
|
|
|
mux::VideoCodecId::VP9
|
|
|
- } else if ctx.format == CodecFormat::VP8 {
|
|
|
+ } else if ctx2.format == CodecFormat::VP8 {
|
|
|
mux::VideoCodecId::VP8
|
|
|
} else {
|
|
|
mux::VideoCodecId::AV1
|
|
|
},
|
|
|
);
|
|
|
- if ctx.format == CodecFormat::AV1 {
|
|
|
+ if ctx2.format == CodecFormat::AV1 {
|
|
|
// [129, 8, 12, 0] in 3.6.0, but zero works
|
|
|
let codec_private = vec![0, 0, 0, 0];
|
|
|
if !webm.set_codec_private(vt.track_number(), &codec_private) {
|
|
@@ -278,6 +303,7 @@ impl RecorderApi for WebmRecorder {
|
|
|
vt,
|
|
|
webm: Some(webm),
|
|
|
ctx,
|
|
|
+ ctx2,
|
|
|
key: false,
|
|
|
written: false,
|
|
|
start: Instant::now(),
|
|
@@ -307,7 +333,7 @@ impl Drop for WebmRecorder {
|
|
|
let _ = std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None));
|
|
|
let mut state = RecordState::WriteTail;
|
|
|
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
|
|
|
- std::fs::remove_file(&self.ctx.filename).ok();
|
|
|
+ std::fs::remove_file(&self.ctx2.filename).ok();
|
|
|
state = RecordState::RemoveFile;
|
|
|
}
|
|
|
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|
|
@@ -318,6 +344,7 @@ impl Drop for WebmRecorder {
|
|
|
struct HwRecorder {
|
|
|
muxer: Muxer,
|
|
|
ctx: RecorderContext,
|
|
|
+ ctx2: RecorderContext2,
|
|
|
written: bool,
|
|
|
key: bool,
|
|
|
start: Instant,
|
|
@@ -325,18 +352,19 @@ struct HwRecorder {
|
|
|
|
|
|
#[cfg(feature = "hwcodec")]
|
|
|
impl RecorderApi for HwRecorder {
|
|
|
- fn new(ctx: RecorderContext) -> ResultType<Self> {
|
|
|
+ fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self> {
|
|
|
let muxer = Muxer::new(MuxContext {
|
|
|
- filename: ctx.filename.clone(),
|
|
|
- width: ctx.width,
|
|
|
- height: ctx.height,
|
|
|
- is265: ctx.format == CodecFormat::H265,
|
|
|
+ filename: ctx2.filename.clone(),
|
|
|
+ width: ctx2.width,
|
|
|
+ height: ctx2.height,
|
|
|
+ is265: ctx2.format == CodecFormat::H265,
|
|
|
framerate: crate::hwcodec::DEFAULT_FPS as _,
|
|
|
})
|
|
|
.map_err(|_| anyhow!("Failed to create hardware muxer"))?;
|
|
|
Ok(HwRecorder {
|
|
|
muxer,
|
|
|
ctx,
|
|
|
+ ctx2,
|
|
|
written: false,
|
|
|
key: false,
|
|
|
start: Instant::now(),
|
|
@@ -365,7 +393,7 @@ impl Drop for HwRecorder {
|
|
|
self.muxer.write_tail().ok();
|
|
|
let mut state = RecordState::WriteTail;
|
|
|
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
|
|
|
- std::fs::remove_file(&self.ctx.filename).ok();
|
|
|
+ std::fs::remove_file(&self.ctx2.filename).ok();
|
|
|
state = RecordState::RemoveFile;
|
|
|
}
|
|
|
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|