auto record outgoing (#9711)

* Add option auto record outgoing session
* In the same connection, all displays and all windows share the same
  recording state.

todo:

Android check external storage permission

Known issue:

* Sciter old issue, stop the process directly without stop record, the record file can't play.

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages
2024-10-21 14:34:06 +08:00
committed by GitHub
parent 289076aa70
commit e8187588c1
65 changed files with 442 additions and 322 deletions

View File

@@ -62,4 +62,3 @@ gstreamer-video = { version = "0.16", optional = true }
git = "https://github.com/rustdesk-org/hwcodec"
optional = true

View File

@@ -15,7 +15,7 @@ use crate::{
aom::{self, AomDecoder, AomEncoder, AomEncoderConfig},
common::GoogleImage,
vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId},
CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb,
CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, ImageTexture,
};
use hbb_common::{
@@ -623,7 +623,7 @@ impl Decoder {
&mut self,
frame: &video_frame::Union,
rgb: &mut ImageRgb,
_texture: &mut *mut c_void,
_texture: &mut ImageTexture,
_pixelbuffer: &mut bool,
chroma: &mut Option<Chroma>,
) -> ResultType<bool> {
@@ -777,12 +777,16 @@ impl Decoder {
fn handle_vram_video_frame(
decoder: &mut VRamDecoder,
frames: &EncodedVideoFrames,
texture: &mut *mut c_void,
texture: &mut ImageTexture,
) -> ResultType<bool> {
let mut ret = false;
for h26x in frames.frames.iter() {
for image in decoder.decode(&h26x.data)? {
*texture = image.frame.texture;
*texture = ImageTexture {
texture: image.frame.texture,
w: image.frame.width as _,
h: image.frame.height as _,
};
ret = true;
}
}

View File

@@ -96,6 +96,22 @@ impl ImageRgb {
}
}
pub struct ImageTexture {
pub texture: *mut c_void,
pub w: usize,
pub h: usize,
}
impl Default for ImageTexture {
fn default() -> Self {
Self {
texture: std::ptr::null_mut(),
w: 0,
h: 0,
}
}
}
#[inline]
pub fn would_block_if_equal(old: &mut Vec<u8>, b: &[u8]) -> std::io::Result<()> {
// does this really help?
@@ -296,6 +312,19 @@ impl From<&VideoFrame> for CodecFormat {
}
}
impl From<&video_frame::Union> for CodecFormat {
fn from(it: &video_frame::Union) -> Self {
match it {
video_frame::Union::Vp8s(_) => CodecFormat::VP8,
video_frame::Union::Vp9s(_) => CodecFormat::VP9,
video_frame::Union::Av1s(_) => CodecFormat::AV1,
video_frame::Union::H264s(_) => CodecFormat::H264,
video_frame::Union::H265s(_) => CodecFormat::H265,
_ => CodecFormat::Unknown,
}
}
}
impl From<&CodecName> for CodecFormat {
fn from(value: &CodecName) -> Self {
match value {

View File

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