feat(recording): add visibility and service storage options (#15662)

* feat(recording): add visibility and service storage options

  - support hide-recording-button in Flutter and Sciter
  - allow a custom save directory for Windows service recordings
  - sanitize peer IDs used in recording filenames

  Tested:
  - with hide-recording-button=Y and allow-auto-record-outgoing=Y,
    outgoing sessions are recorded automatically while the recording button
    remains hidden and cannot be stopped from the UI; verified on Flutter
    desktop, Sciter, and Android
  - windows-service-video-save-directory takes effect when the Windows client
    runs as an installed service
  - the Windows controlling side can save recordings for direct IP:port
    connections

Signed-off-by: 21pages <sunboeasy@gmail.com>

* update hbb_common

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(recording): validate configured save directories

  - trim configured recording directory paths
  - reject non-absolute paths and fall back to defaults
  - warn when a non-empty path is invalid

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(recording): validate configured save directories

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages
2026-07-25 15:21:13 +08:00
committed by GitHub
parent ad9dac1001
commit cefff781d4
9 changed files with 123 additions and 6 deletions

View File

@@ -20,6 +20,22 @@ use webm::mux::{self, Segment, Track, VideoTrack, Writer};
const MIN_SECS: u64 = 1;
// Replace characters that are invalid in Windows filename components so recordings remain portable.
// Control characters are also replaced because they can make filenames invalid
// on Windows or invisible and difficult to handle on Linux and macOS.
fn sanitize_filename_component(value: &str) -> String {
value
.chars()
.map(|c| {
if c.is_control() || matches!(c, '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*') {
'_'
} else {
c
}
})
.collect()
}
#[derive(Debug, Clone)]
pub struct RecorderContext {
pub server: bool,
@@ -45,7 +61,7 @@ impl RecorderContext2 {
}
let file = if ctx.server { "incoming" } else { "outgoing" }.to_string()
+ "_"
+ &ctx.id.clone()
+ &sanitize_filename_component(&ctx.id)
+ &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string()
+ &format!(
"{}{}_",
@@ -421,3 +437,24 @@ impl Drop for HwRecorder {
self.ctx.tx.as_ref().map(|tx| tx.send(state));
}
}
#[cfg(test)]
mod tests {
use super::sanitize_filename_component;
#[test]
fn sanitize_recording_filename_component() {
assert_eq!(
sanitize_filename_component("192.168.1.2:21118"),
"192.168.1.2_21118"
);
assert_eq!(
sanitize_filename_component("[2001:db8::1]:21118"),
"[2001_db8__1]_21118"
);
assert_eq!(
sanitize_filename_component("peer/name\\with?bad\nchars"),
"peer_name_with_bad_chars"
);
}
}