fix & perf: Comprehensive codebase optimization, timestamp rescaling, GOP/bitrate tuning, and UI polish
This commit is contained in:
parent
e46725e66d
commit
bc3d91d1b7
13 changed files with 418 additions and 151 deletions
|
|
@ -31,6 +31,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let (width, height) = match &first_frame {
|
||||
Frame::Raw { width, height, .. } => (*width, *height),
|
||||
Frame::DmaBuf { width, height, .. } => (*width, *height),
|
||||
#[cfg(target_os = "windows")]
|
||||
Frame::D3D11Texture { .. } => (1920, 1080),
|
||||
};
|
||||
|
||||
let (frame_tx, frame_rx) = bounded::<Frame>(5);
|
||||
|
|
@ -59,15 +61,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
});
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut encoder = VaapiEncoder::new(width, height).expect("Failed to init encoder");
|
||||
let mut config = vrec::config::VrecConfig::load();
|
||||
let mut encoder = VaapiEncoder::new_with_bitrate(width, height, config.record_bitrate_kbps)
|
||||
.expect("Failed to init encoder");
|
||||
let codec_ctx_ptr = encoder.codec_ctx() as usize;
|
||||
|
||||
let mut audio_encoder = if let Some(ref info) = audio_info {
|
||||
vrec::encoder::AudioEncoder::new(info.1 as i32, info.2 as i32).ok()
|
||||
} else { None };
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let audio_codec_ctx_ptr = audio_encoder.as_ref().map(|e| e.codec_ctx() as usize);
|
||||
|
||||
let mut config = vrec::config::VrecConfig::load();
|
||||
let mut ring = HeapRb::<Packet>::new((config.replay_duration_sec * 120).max(120) as usize);
|
||||
let mut normal_muxer: Option<Muxer> = None;
|
||||
let mut normal_recording = false;
|
||||
|
|
@ -94,7 +99,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let _ = muxer.write_packet(&p);
|
||||
}
|
||||
let _ = muxer.finalize();
|
||||
println!("Replay saved!");
|
||||
println!("Replay saved to {}!", name);
|
||||
} else {
|
||||
println!("No keyframe found in buffer, skipping save.");
|
||||
}
|
||||
|
|
@ -110,7 +115,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let new_capacity = (config.replay_duration_sec * 120).max(120) as usize;
|
||||
if ring.capacity().get() != new_capacity {
|
||||
ring = HeapRb::<Packet>::new(new_capacity);
|
||||
println!("Replay buffer resized to {} frames.", new_capacity);
|
||||
println!("Replay buffer resized to {} packets.", new_capacity);
|
||||
}
|
||||
},
|
||||
Command::SaveReplay => {
|
||||
|
|
@ -120,7 +125,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
}
|
||||
},
|
||||
Command::StartRecording => {
|
||||
if config.record_enabled && !normal_recording {
|
||||
if !normal_recording {
|
||||
normal_recording = true;
|
||||
normal_waiting_keyframe = true;
|
||||
println!("StartRecording requested, waiting for keyframe...");
|
||||
|
|
@ -132,27 +137,40 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
println!("Stopped normal recording.");
|
||||
}
|
||||
normal_recording = false;
|
||||
normal_waiting_keyframe = false;
|
||||
},
|
||||
Command::ToggleRecording => {
|
||||
if normal_recording {
|
||||
if let Some(mut m) = normal_muxer.take() {
|
||||
let _ = m.finalize();
|
||||
println!("Stopped normal recording.");
|
||||
}
|
||||
normal_recording = false;
|
||||
normal_waiting_keyframe = false;
|
||||
} else {
|
||||
normal_recording = true;
|
||||
normal_waiting_keyframe = true;
|
||||
println!("ToggleRecording: StartRecording requested, waiting for keyframe...");
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
while let Ok(audio_chunk) = audio_rx.try_recv() {
|
||||
if let Some(enc) = audio_encoder.as_mut() {
|
||||
if let Ok(audio_packets) = enc.encode_pcm(&audio_chunk) {
|
||||
if let Some(enc) = audio_encoder.as_mut()
|
||||
&& let Ok(audio_packets) = enc.encode_pcm(&audio_chunk) {
|
||||
for mut pkt in audio_packets {
|
||||
pkt.set_stream_index(1);
|
||||
if config.replay_enabled {
|
||||
ring.push_overwrite(pkt.clone());
|
||||
}
|
||||
if normal_recording && !normal_waiting_keyframe {
|
||||
if let Some(muxer) = normal_muxer.as_mut() {
|
||||
if normal_recording && !normal_waiting_keyframe
|
||||
&& let Some(muxer) = normal_muxer.as_mut() {
|
||||
let _ = muxer.write_packet(&pkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(packets) = encoder.encode_frame(&frame) {
|
||||
|
|
@ -162,23 +180,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
ring.push_overwrite(pkt.clone());
|
||||
}
|
||||
if normal_recording {
|
||||
if normal_waiting_keyframe {
|
||||
if pkt.is_keyframe() {
|
||||
normal_waiting_keyframe = false;
|
||||
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
|
||||
let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
let name = format!("record_{}.mp4", ts);
|
||||
let audio_codec_ctx = audio_codec_ctx_ptr.map(|p| p as *mut ffmpeg_next::ffi::AVCodecContext);
|
||||
normal_muxer = unsafe { Muxer::new(&name, codec_ctx, audio_codec_ctx).ok() };
|
||||
println!("Started normal recording to {}", name);
|
||||
}
|
||||
if normal_waiting_keyframe && pkt.is_keyframe() {
|
||||
normal_waiting_keyframe = false;
|
||||
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
|
||||
let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
let name = format!("record_{}.mp4", ts);
|
||||
let audio_codec_ctx = audio_codec_ctx_ptr.map(|p| p as *mut ffmpeg_next::ffi::AVCodecContext);
|
||||
normal_muxer = unsafe { Muxer::new(&name, codec_ctx, audio_codec_ctx).ok() };
|
||||
println!("Started normal recording to {}", name);
|
||||
}
|
||||
|
||||
if !normal_waiting_keyframe {
|
||||
if let Some(muxer) = normal_muxer.as_mut() {
|
||||
if !normal_waiting_keyframe
|
||||
&& let Some(muxer) = normal_muxer.as_mut() {
|
||||
let _ = muxer.write_packet(&pkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,40 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
match args[1].as_str() {
|
||||
"--save" => {
|
||||
vrec::overlay::show_notification_overlay();
|
||||
return send_command(Command::SaveReplay).map_err(|e| e);
|
||||
return send_command(Command::SaveReplay);
|
||||
}
|
||||
"--menu" => {
|
||||
vrec::overlay::show_menu_overlay();
|
||||
return Ok(());
|
||||
}
|
||||
"--record" | "--toggle" => {
|
||||
return send_command(Command::ToggleRecording);
|
||||
}
|
||||
"--start" => {
|
||||
return send_command(Command::StartRecording);
|
||||
}
|
||||
"--stop" => {
|
||||
return send_command(Command::StopRecording);
|
||||
}
|
||||
"--reload" => {
|
||||
return send_command(Command::ReloadConfig);
|
||||
}
|
||||
"--quit" => {
|
||||
return send_command(Command::StopDaemon);
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
println!("vrec-ui - UI Overlay and Hotkey listener for vrec");
|
||||
println!("Usage:");
|
||||
println!(" vrec-ui Run global hotkey manager in background");
|
||||
println!(" vrec-ui --menu Open GPU Screen Recorder style menu overlay");
|
||||
println!(" vrec-ui --save Save instant replay and show notification");
|
||||
println!(" vrec-ui --record Toggle normal recording on/off");
|
||||
println!(" vrec-ui --start Start normal recording");
|
||||
println!(" vrec-ui --stop Stop normal recording");
|
||||
println!(" vrec-ui --reload Reload daemon configuration");
|
||||
println!(" vrec-ui --quit Stop background daemon");
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,28 +56,40 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
|
||||
if session_type.to_lowercase() == "wayland" {
|
||||
if is_gnome || is_kde {
|
||||
println!("WARNING: Wayland native layer shell used. Ensure your compositor allows it.");
|
||||
println!("Note: Global hotkeys may fail to register on Wayland compositors.");
|
||||
println!("Note: Wayland native layer shell used. Ensure your compositor allows it.");
|
||||
println!("Tip: Global hotkeys may be restricted by Wayland compositors.");
|
||||
println!(" You can bind `vrec-ui --menu` or `vrec-ui --save` in your system shortcut settings.");
|
||||
} else {
|
||||
println!("wlroots-based compositor detected.");
|
||||
println!("wlroots/Hyprland compositor detected.");
|
||||
}
|
||||
} else {
|
||||
println!("X11 session detected.");
|
||||
}
|
||||
|
||||
let manager = GlobalHotKeyManager::new().unwrap();
|
||||
let manager = GlobalHotKeyManager::new().map_err(|e| format!("Failed to init GlobalHotKeyManager: {:?}", e))?;
|
||||
let config = vrec::config::VrecConfig::load();
|
||||
let save_hotkey = vrec::hotkey::parse_hotkey(&config.save_hotkey)
|
||||
.unwrap_or_else(|| HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR));
|
||||
let menu_hotkey = vrec::hotkey::parse_hotkey(&config.menu_hotkey)
|
||||
.unwrap_or_else(|| HotKey::new(Some(Modifiers::ALT), Code::KeyZ));
|
||||
|
||||
let mut registered_count = 0;
|
||||
if let Err(e) = manager.register(save_hotkey) {
|
||||
eprintln!("Failed to register global hotkey! Wayland compositor might be blocking it: {}", e);
|
||||
eprintln!("Fallback: running without global hotkey. Send commands via socket manually.");
|
||||
eprintln!("Warning: Failed to register save hotkey ({}): {}", config.save_hotkey, e);
|
||||
} else {
|
||||
manager.register(menu_hotkey).unwrap();
|
||||
println!("Registered global hotkeys:\n - Ctrl+Shift+R (Save Replay)\n - Alt+Z (Open GUI Menu)");
|
||||
println!("Registered save replay hotkey: {}", config.save_hotkey);
|
||||
registered_count += 1;
|
||||
}
|
||||
|
||||
if let Err(e) = manager.register(menu_hotkey) {
|
||||
eprintln!("Warning: Failed to register menu hotkey ({}): {}", config.menu_hotkey, e);
|
||||
} else {
|
||||
println!("Registered menu overlay hotkey: {}", config.menu_hotkey);
|
||||
registered_count += 1;
|
||||
}
|
||||
|
||||
if registered_count == 0 {
|
||||
eprintln!("Note: Running without global hotkeys. You can bind `vrec-ui --menu` and `vrec-ui --save` to compositor shortcuts.");
|
||||
}
|
||||
|
||||
let receiver = GlobalHotKeyEvent::receiver();
|
||||
|
|
@ -57,11 +97,11 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
if let Ok(event) = receiver.recv()
|
||||
&& event.state == global_hotkey::HotKeyState::Pressed {
|
||||
if event.id == save_hotkey.id() {
|
||||
println!("Hotkey triggered! Sending SaveReplay command to daemon...");
|
||||
println!("Save replay hotkey pressed. Triggering SaveReplay...");
|
||||
vrec::overlay::show_notification_overlay();
|
||||
let _ = send_command(Command::SaveReplay);
|
||||
} else if event.id == menu_hotkey.id() {
|
||||
println!("Alt+Z triggered! Opening GUI Menu Overlay...");
|
||||
println!("Menu hotkey pressed. Opening Overlay Menu...");
|
||||
vrec::overlay::show_menu_overlay();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ pub struct AudioCapture {
|
|||
}
|
||||
|
||||
impl AudioCapture {
|
||||
pub fn new(sender: Sender<Vec<f32>>) -> Result<(Self, u32, u16), Box<dyn std::error::Error>> {
|
||||
pub fn new(sender: Sender<Vec<f32>>) -> Result<(Self, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let host = cpal::default_host();
|
||||
let device = host.default_input_device().ok_or("No audio input device available")?;
|
||||
|
||||
|
|
@ -17,12 +17,12 @@ impl AudioCapture {
|
|||
println!("Audio capture initialized: {} Hz, {} channels", sample_rate, channels);
|
||||
|
||||
let err_fn = |err| eprintln!("Audio stream error: {}", err);
|
||||
let stream_config: cpal::StreamConfig = config.clone().into();
|
||||
let stream_config: cpal::StreamConfig = config.into();
|
||||
|
||||
let stream = match config.sample_format() {
|
||||
cpal::SampleFormat::F32 => {
|
||||
device.build_input_stream(
|
||||
stream_config.clone(),
|
||||
stream_config,
|
||||
move |data: &[f32], _: &_| {
|
||||
let _ = sender.try_send(data.to_vec());
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ impl MockCapture {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for MockCapture {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl FrameSource for MockCapture {
|
||||
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
|
||||
std::thread::sleep(Duration::from_millis(16)); // ~60fps
|
||||
|
|
|
|||
|
|
@ -8,8 +8,21 @@ pub struct WaylandCapture {
|
|||
|
||||
impl WaylandCapture {
|
||||
pub async fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let proxy = Screencast::new().await?;
|
||||
let session = proxy.create_session(Default::default()).await?;
|
||||
let proxy = Screencast::new().await.map_err(|e| {
|
||||
format!(
|
||||
"Failed to initialize XDG ScreenCast portal ({:?}).\n\
|
||||
Ensure xdg-desktop-portal and your compositor's backend are installed and running:\n\
|
||||
- Hyprland: xdg-desktop-portal-hyprland\n\
|
||||
- Sway / wlroots: xdg-desktop-portal-wlr\n\
|
||||
- GNOME: xdg-desktop-portal-gnome\n\
|
||||
- KDE Plasma: xdg-desktop-portal-kde",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
let session = proxy.create_session(Default::default()).await.map_err(|e| {
|
||||
format!("Failed to create ScreenCast portal session: {:?}", e)
|
||||
})?;
|
||||
|
||||
let select_opts = ashpd::desktop::screencast::SelectSourcesOptions::default()
|
||||
.set_multiple(true)
|
||||
|
|
@ -17,14 +30,20 @@ impl WaylandCapture {
|
|||
.set_sources(SourceType::Monitor | SourceType::Window)
|
||||
.set_persist_mode(PersistMode::DoNot);
|
||||
|
||||
proxy.select_sources(&session, select_opts).await?;
|
||||
proxy.select_sources(&session, select_opts).await.map_err(|e| {
|
||||
format!("Failed to select ScreenCast sources: {:?}", e)
|
||||
})?;
|
||||
|
||||
let response = proxy.start(&session, None, Default::default()).await?.response()?;
|
||||
let response = proxy.start(&session, None, Default::default()).await
|
||||
.map_err(|e| format!("Failed to start ScreenCast session: {:?}", e))?
|
||||
.response()
|
||||
.map_err(|e| format!("Failed to get ScreenCast response: {:?}", e))?;
|
||||
|
||||
let stream = response.streams().first().ok_or("No stream")?;
|
||||
let stream = response.streams().first().ok_or("No ScreenCast streams returned by portal")?;
|
||||
let node_id = stream.pipe_wire_node_id();
|
||||
|
||||
let fd = proxy.open_pipe_wire_remote(&session, Default::default()).await?;
|
||||
let fd = proxy.open_pipe_wire_remote(&session, Default::default()).await
|
||||
.map_err(|e| format!("Failed to open PipeWire remote descriptor: {:?}", e))?;
|
||||
|
||||
let stream = crate::capture::wayland_stream::PipeWireStream::new(node_id, fd)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ impl FrameSource for X11Capture {
|
|||
y: 0,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
plane_mask: std::u32::MAX,
|
||||
plane_mask: u32::MAX,
|
||||
});
|
||||
|
||||
let reply = self.conn.wait_for_reply(cookie)?;
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ impl VrecConfig {
|
|||
pub fn load() -> Self {
|
||||
let path = Self::config_path();
|
||||
if path.exists()
|
||||
&& let Ok(data) = fs::read_to_string(path)
|
||||
&& let Ok(config) = serde_json::from_str(&data) {
|
||||
return config;
|
||||
}
|
||||
&& let Ok(data) = fs::read_to_string(&path)
|
||||
&& let Ok(config) = serde_json::from_str(&data) {
|
||||
return config;
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
|
|
@ -66,15 +66,23 @@ impl VrecConfig {
|
|||
}
|
||||
|
||||
pub fn notify_daemon_reload() {
|
||||
use std::env;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::io::Write;
|
||||
let socket_path = format!("{}/vrec.sock", env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string()));
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket_path)
|
||||
&& let Ok(payload) = serde_json::to_vec(&crate::ipc::Command::ReloadConfig) {
|
||||
let len_buf = (payload.len() as u32).to_le_bytes();
|
||||
let _ = stream.write_all(&len_buf);
|
||||
let _ = stream.write_all(&payload);
|
||||
}
|
||||
let _ = crate::ipc::send_command(crate::ipc::Command::ReloadConfig);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_config_defaults_and_json() {
|
||||
let cfg = VrecConfig::default();
|
||||
assert!(cfg.replay_enabled);
|
||||
assert_eq!(cfg.replay_duration_sec, 60);
|
||||
let json = serde_json::to_string(&cfg).unwrap();
|
||||
let parsed: VrecConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.menu_hotkey, "Alt+Z");
|
||||
assert_eq!(parsed.save_hotkey, "Ctrl+Shift+R");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
118
src/encoder.rs
118
src/encoder.rs
|
|
@ -6,13 +6,18 @@ pub struct VaapiEncoder {
|
|||
codec_ctx: *mut AVCodecContext,
|
||||
hw_device_ctx: *mut AVBufferRef,
|
||||
hw_frames_ctx: *mut AVBufferRef,
|
||||
sws_ctx: *mut SwsContext,
|
||||
next_pts: i64,
|
||||
}
|
||||
|
||||
impl VaapiEncoder {
|
||||
pub fn new(width: u32, height: u32) -> Result<Self, String> {
|
||||
Self::new_with_bitrate(width, height, 15_000)
|
||||
}
|
||||
|
||||
pub fn new_with_bitrate(width: u32, height: u32, bitrate_kbps: u32) -> Result<Self, String> {
|
||||
unsafe {
|
||||
let codec = avcodec_find_encoder_by_name(b"h264_vaapi\0".as_ptr() as *const i8);
|
||||
let codec = avcodec_find_encoder_by_name(c"h264_vaapi".as_ptr());
|
||||
if codec.is_null() {
|
||||
return Err("h264_vaapi encoder not found".into());
|
||||
}
|
||||
|
|
@ -27,6 +32,9 @@ impl VaapiEncoder {
|
|||
(*codec_ctx).time_base = AVRational { num: 1, den: 60 };
|
||||
(*codec_ctx).framerate = AVRational { num: 60, den: 1 };
|
||||
(*codec_ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_VAAPI;
|
||||
(*codec_ctx).gop_size = 60; // Emit IDR keyframe every 60 frames (1 second)
|
||||
(*codec_ctx).max_b_frames = 0; // Low latency, no B-frame reordering
|
||||
(*codec_ctx).bit_rate = (bitrate_kbps as i64) * 1000;
|
||||
|
||||
let mut hw_device_ctx: *mut AVBufferRef = ptr::null_mut();
|
||||
let ret = av_hwdevice_ctx_create(
|
||||
|
|
@ -37,11 +45,14 @@ impl VaapiEncoder {
|
|||
0,
|
||||
);
|
||||
if ret < 0 {
|
||||
avcodec_free_context(&mut (codec_ctx as *mut _));
|
||||
return Err("Failed to create VAAPI hardware device".into());
|
||||
}
|
||||
|
||||
let hw_frames_ref = av_hwframe_ctx_alloc(hw_device_ctx);
|
||||
if hw_frames_ref.is_null() {
|
||||
av_buffer_unref(&mut hw_device_ctx);
|
||||
avcodec_free_context(&mut (codec_ctx as *mut _));
|
||||
return Err("Failed to allocate hw frames context".into());
|
||||
}
|
||||
let frames_ctx = (*hw_frames_ref).data as *mut AVHWFramesContext;
|
||||
|
|
@ -53,6 +64,9 @@ impl VaapiEncoder {
|
|||
|
||||
let ret = av_hwframe_ctx_init(hw_frames_ref);
|
||||
if ret < 0 {
|
||||
av_buffer_unref(&mut (hw_frames_ref as *mut _));
|
||||
av_buffer_unref(&mut hw_device_ctx);
|
||||
avcodec_free_context(&mut (codec_ctx as *mut _));
|
||||
return Err("Failed to init hw frames context".into());
|
||||
}
|
||||
|
||||
|
|
@ -60,6 +74,9 @@ impl VaapiEncoder {
|
|||
|
||||
let ret = avcodec_open2(codec_ctx, codec, ptr::null_mut());
|
||||
if ret < 0 {
|
||||
av_buffer_unref(&mut (hw_frames_ref as *mut _));
|
||||
av_buffer_unref(&mut hw_device_ctx);
|
||||
avcodec_free_context(&mut (codec_ctx as *mut _));
|
||||
return Err("Failed to open h264_vaapi encoder".into());
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +84,7 @@ impl VaapiEncoder {
|
|||
codec_ctx,
|
||||
hw_device_ctx,
|
||||
hw_frames_ctx: hw_frames_ref,
|
||||
sws_ctx: ptr::null_mut(),
|
||||
next_pts: 0,
|
||||
})
|
||||
}
|
||||
|
|
@ -103,14 +121,16 @@ impl VaapiEncoder {
|
|||
(*nv12_frame).height = *height as i32;
|
||||
av_frame_get_buffer(nv12_frame, 32);
|
||||
|
||||
let sws_ctx = sws_getContext(
|
||||
*width as i32, *height as i32, AVPixelFormat::AV_PIX_FMT_BGRA,
|
||||
*width as i32, *height as i32, AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
2, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()
|
||||
);
|
||||
if self.sws_ctx.is_null() {
|
||||
self.sws_ctx = sws_getContext(
|
||||
*width as i32, *height as i32, AVPixelFormat::AV_PIX_FMT_BGRA,
|
||||
*width as i32, *height as i32, AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
2, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()
|
||||
);
|
||||
}
|
||||
|
||||
sws_scale(
|
||||
sws_ctx,
|
||||
self.sws_ctx,
|
||||
(*bgra_frame).data.as_ptr() as *const *const u8,
|
||||
(*bgra_frame).linesize.as_ptr(),
|
||||
0,
|
||||
|
|
@ -118,7 +138,6 @@ impl VaapiEncoder {
|
|||
(*nv12_frame).data.as_ptr(),
|
||||
(*nv12_frame).linesize.as_ptr()
|
||||
);
|
||||
sws_freeContext(sws_ctx);
|
||||
|
||||
let mut hw_frame = av_frame_alloc();
|
||||
av_hwframe_get_buffer(self.hw_frames_ctx, hw_frame, 0);
|
||||
|
|
@ -145,7 +164,7 @@ impl VaapiEncoder {
|
|||
nb_objects: 1,
|
||||
objects: [AVDRMObjectDescriptor {
|
||||
fd: *fd,
|
||||
size: 0, // usually ignored or mapped by driver
|
||||
size: 0,
|
||||
format_modifier: *modifier,
|
||||
}, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()],
|
||||
nb_layers: 1,
|
||||
|
|
@ -169,11 +188,6 @@ impl VaapiEncoder {
|
|||
let mut hw_frame = av_frame_alloc();
|
||||
(*hw_frame).format = AVPixelFormat::AV_PIX_FMT_VAAPI as i32;
|
||||
|
||||
// Map the DRM frame to VAAPI
|
||||
// Since it's hardware mapping, av_hwframe_map expects the device context or frames context.
|
||||
// Wait, av_hwframe_map uses the destination's hw_frames_ctx if we preallocate, OR
|
||||
// if we just pass a device context to hw_frame.hw_frames_ctx?
|
||||
// Let's allocate the hw_frame from our VAAPI frames context.
|
||||
let ret = av_hwframe_get_buffer(self.hw_frames_ctx, hw_frame, 0);
|
||||
if ret >= 0 {
|
||||
let map_ret = av_hwframe_map(hw_frame, drm_frame, 0);
|
||||
|
|
@ -195,9 +209,6 @@ impl VaapiEncoder {
|
|||
}
|
||||
}
|
||||
|
||||
// For AV_PIX_FMT_DRM_PRIME, we don't own the data, so don't av_frame_unref the data buffer,
|
||||
// av_frame_free is safe because data[0] wasn't allocated by ffmpeg.
|
||||
// Wait, av_frame_free will try to free data[0] if buf[0] is set. But we didn't set buf[0].
|
||||
av_frame_free(&mut hw_frame);
|
||||
av_frame_free(&mut drm_frame);
|
||||
}
|
||||
|
|
@ -214,9 +225,19 @@ impl VaapiEncoder {
|
|||
impl Drop for VaapiEncoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
avcodec_free_context(&mut self.codec_ctx);
|
||||
av_buffer_unref(&mut self.hw_device_ctx);
|
||||
av_buffer_unref(&mut self.hw_frames_ctx);
|
||||
if !self.sws_ctx.is_null() {
|
||||
sws_freeContext(self.sws_ctx);
|
||||
self.sws_ctx = ptr::null_mut();
|
||||
}
|
||||
if !self.codec_ctx.is_null() {
|
||||
avcodec_free_context(&mut self.codec_ctx);
|
||||
}
|
||||
if !self.hw_device_ctx.is_null() {
|
||||
av_buffer_unref(&mut self.hw_device_ctx);
|
||||
}
|
||||
if !self.hw_frames_ctx.is_null() {
|
||||
av_buffer_unref(&mut self.hw_frames_ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -224,6 +245,7 @@ impl Drop for VaapiEncoder {
|
|||
pub struct AudioEncoder {
|
||||
codec_ctx: *mut AVCodecContext,
|
||||
swr_ctx: *mut SwrContext,
|
||||
enc_frame: *mut AVFrame,
|
||||
next_pts: i64,
|
||||
frame_size: i32,
|
||||
channels: i32,
|
||||
|
|
@ -239,22 +261,23 @@ impl AudioEncoder {
|
|||
}
|
||||
|
||||
let codec_ctx = avcodec_alloc_context3(codec);
|
||||
if codec_ctx.is_null() {
|
||||
return Err("Failed to allocate AAC codec context".into());
|
||||
}
|
||||
|
||||
(*codec_ctx).sample_rate = sample_rate;
|
||||
(*codec_ctx).time_base = AVRational { num: 1, den: sample_rate };
|
||||
(*codec_ctx).ch_layout.nb_channels = channels;
|
||||
// AAC requires FLTP (planar float)
|
||||
(*codec_ctx).sample_fmt = AVSampleFormat::AV_SAMPLE_FMT_FLTP;
|
||||
(*codec_ctx).bit_rate = 192_000;
|
||||
// For older ffmpeg compatibility:
|
||||
av_channel_layout_default(&mut (*codec_ctx).ch_layout, channels);
|
||||
|
||||
let ret = avcodec_open2(codec_ctx, codec, std::ptr::null_mut());
|
||||
let ret = avcodec_open2(codec_ctx, codec, ptr::null_mut());
|
||||
if ret < 0 {
|
||||
avcodec_free_context(&mut (codec_ctx as *mut _));
|
||||
return Err("Failed to open AAC encoder".into());
|
||||
}
|
||||
|
||||
// Create SwrContext to convert from interleaved F32 (AV_SAMPLE_FMT_FLT) to planar F32 (AV_SAMPLE_FMT_FLTP)
|
||||
let mut swr_ctx: *mut SwrContext = std::ptr::null_mut();
|
||||
let mut swr_ctx: *mut SwrContext = ptr::null_mut();
|
||||
swr_alloc_set_opts2(
|
||||
&mut swr_ctx,
|
||||
&(*codec_ctx).ch_layout,
|
||||
|
|
@ -264,16 +287,24 @@ impl AudioEncoder {
|
|||
AVSampleFormat::AV_SAMPLE_FMT_FLT,
|
||||
sample_rate,
|
||||
0,
|
||||
std::ptr::null_mut()
|
||||
ptr::null_mut()
|
||||
);
|
||||
swr_init(swr_ctx);
|
||||
|
||||
let frame_size = (*codec_ctx).frame_size;
|
||||
let fifo = av_audio_fifo_alloc(AVSampleFormat::AV_SAMPLE_FMT_FLTP, channels, 1024 * 10);
|
||||
let fifo = av_audio_fifo_alloc(AVSampleFormat::AV_SAMPLE_FMT_FLTP, channels, 1024 * 32);
|
||||
|
||||
let enc_frame = av_frame_alloc();
|
||||
(*enc_frame).nb_samples = frame_size;
|
||||
(*enc_frame).format = AVSampleFormat::AV_SAMPLE_FMT_FLTP as i32;
|
||||
(*enc_frame).ch_layout = (*codec_ctx).ch_layout;
|
||||
(*enc_frame).sample_rate = sample_rate;
|
||||
av_frame_get_buffer(enc_frame, 0);
|
||||
|
||||
Ok(Self {
|
||||
codec_ctx,
|
||||
swr_ctx,
|
||||
enc_frame,
|
||||
next_pts: 0,
|
||||
frame_size,
|
||||
channels,
|
||||
|
|
@ -325,19 +356,12 @@ impl AudioEncoder {
|
|||
|
||||
// Read exactly frame_size chunks from FIFO and encode
|
||||
while av_audio_fifo_size(self.fifo) >= self.frame_size {
|
||||
let mut enc_frame = av_frame_alloc();
|
||||
(*enc_frame).nb_samples = self.frame_size;
|
||||
(*enc_frame).format = AVSampleFormat::AV_SAMPLE_FMT_FLTP as i32;
|
||||
(*enc_frame).ch_layout = (*self.codec_ctx).ch_layout;
|
||||
(*enc_frame).sample_rate = (*self.codec_ctx).sample_rate;
|
||||
av_frame_get_buffer(enc_frame, 0);
|
||||
|
||||
av_audio_fifo_read(self.fifo, (*enc_frame).data.as_mut_ptr() as *mut *mut std::ffi::c_void, self.frame_size);
|
||||
av_audio_fifo_read(self.fifo, (*self.enc_frame).data.as_mut_ptr() as *mut *mut std::ffi::c_void, self.frame_size);
|
||||
|
||||
(*enc_frame).pts = self.next_pts;
|
||||
(*self.enc_frame).pts = self.next_pts;
|
||||
self.next_pts += self.frame_size as i64;
|
||||
|
||||
if avcodec_send_frame(self.codec_ctx, enc_frame) >= 0 {
|
||||
if avcodec_send_frame(self.codec_ctx, self.enc_frame) >= 0 {
|
||||
let mut pkt = av_packet_alloc();
|
||||
while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
|
||||
let new_pkt = av_packet_alloc();
|
||||
|
|
@ -346,7 +370,6 @@ impl AudioEncoder {
|
|||
}
|
||||
av_packet_free(&mut pkt);
|
||||
}
|
||||
av_frame_free(&mut enc_frame);
|
||||
}
|
||||
}
|
||||
Ok(packets)
|
||||
|
|
@ -356,9 +379,20 @@ impl AudioEncoder {
|
|||
impl Drop for AudioEncoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
av_audio_fifo_free(self.fifo);
|
||||
swr_free(&mut self.swr_ctx);
|
||||
avcodec_free_context(&mut self.codec_ctx);
|
||||
if !self.enc_frame.is_null() {
|
||||
av_frame_free(&mut self.enc_frame);
|
||||
}
|
||||
if !self.fifo.is_null() {
|
||||
av_audio_fifo_free(self.fifo);
|
||||
self.fifo = ptr::null_mut();
|
||||
}
|
||||
if !self.swr_ctx.is_null() {
|
||||
swr_free(&mut self.swr_ctx);
|
||||
self.swr_ctx = ptr::null_mut();
|
||||
}
|
||||
if !self.codec_ctx.is_null() {
|
||||
avcodec_free_context(&mut self.codec_ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use global_hotkey::hotkey::{HotKey, Modifiers, Code};
|
|||
pub fn parse_hotkey(s: &str) -> Option<HotKey> {
|
||||
let mut mods = Modifiers::empty();
|
||||
let mut code = None;
|
||||
|
||||
for part in s.split('+') {
|
||||
let p = part.trim().to_lowercase();
|
||||
match p.as_str() {
|
||||
|
|
@ -11,7 +12,6 @@ pub fn parse_hotkey(s: &str) -> Option<HotKey> {
|
|||
"alt" => mods |= Modifiers::ALT,
|
||||
"meta" | "super" | "windows" | "cmd" => mods |= Modifiers::META,
|
||||
_ => {
|
||||
// simple mapping
|
||||
code = match p.as_str() {
|
||||
"a" => Some(Code::KeyA),
|
||||
"b" => Some(Code::KeyB),
|
||||
|
|
@ -39,15 +39,61 @@ pub fn parse_hotkey(s: &str) -> Option<HotKey> {
|
|||
"x" => Some(Code::KeyX),
|
||||
"y" => Some(Code::KeyY),
|
||||
"z" => Some(Code::KeyZ),
|
||||
"0" => Some(Code::Digit0),
|
||||
"1" => Some(Code::Digit1),
|
||||
"2" => Some(Code::Digit2),
|
||||
"3" => Some(Code::Digit3),
|
||||
"4" => Some(Code::Digit4),
|
||||
"5" => Some(Code::Digit5),
|
||||
"6" => Some(Code::Digit6),
|
||||
"7" => Some(Code::Digit7),
|
||||
"8" => Some(Code::Digit8),
|
||||
"9" => Some(Code::Digit9),
|
||||
"f1" => Some(Code::F1),
|
||||
"f2" => Some(Code::F2),
|
||||
"f3" => Some(Code::F3),
|
||||
"f4" => Some(Code::F4),
|
||||
"f5" => Some(Code::F5),
|
||||
"f6" => Some(Code::F6),
|
||||
"f7" => Some(Code::F7),
|
||||
"f8" => Some(Code::F8),
|
||||
"f9" => Some(Code::F9),
|
||||
"f10" => Some(Code::F10),
|
||||
"f11" => Some(Code::F11),
|
||||
"f12" => Some(Code::F12),
|
||||
"space" => Some(Code::Space),
|
||||
"escape" | "esc" => Some(Code::Escape),
|
||||
"tab" => Some(Code::Tab),
|
||||
"enter" | "return" => Some(Code::Enter),
|
||||
"backspace" => Some(Code::Backspace),
|
||||
"insert" => Some(Code::Insert),
|
||||
"delete" | "del" => Some(Code::Delete),
|
||||
"home" => Some(Code::Home),
|
||||
"end" => Some(Code::End),
|
||||
"pageup" | "pgup" => Some(Code::PageUp),
|
||||
"pagedown" | "pgdn" => Some(Code::PageDown),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(c) = code {
|
||||
Some(HotKey::new(Some(mods), c))
|
||||
} else {
|
||||
None
|
||||
|
||||
code.map(|c| HotKey::new(Some(mods), c))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_hotkey_combinations() {
|
||||
assert!(parse_hotkey("Ctrl+Shift+R").is_some());
|
||||
assert!(parse_hotkey("Alt+Z").is_some());
|
||||
assert!(parse_hotkey("F12").is_some());
|
||||
assert!(parse_hotkey("Ctrl+Alt+Delete").is_some());
|
||||
assert!(parse_hotkey("super+space").is_some());
|
||||
assert!(parse_hotkey("").is_none());
|
||||
assert!(parse_hotkey("invalid_key_xyz").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Command {
|
||||
SaveReplay,
|
||||
StartRecording,
|
||||
StopRecording,
|
||||
ToggleRecording,
|
||||
ReloadConfig,
|
||||
StopDaemon,
|
||||
ShowOverlay,
|
||||
|
|
@ -14,6 +15,7 @@ pub fn send_command(cmd: Command) -> Result<(), Box<dyn std::error::Error + Send
|
|||
use std::env;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::io::Write;
|
||||
|
||||
let socket_path = format!("{}/vrec.sock", env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string()));
|
||||
let mut stream = UnixStream::connect(&socket_path)?;
|
||||
let payload = serde_json::to_vec(&cmd)?;
|
||||
|
|
|
|||
111
src/muxer.rs
111
src/muxer.rs
|
|
@ -4,80 +4,126 @@ use std::ffi::CString;
|
|||
|
||||
pub struct Muxer {
|
||||
fmt_ctx: *mut AVFormatContext,
|
||||
video_time_base: AVRational,
|
||||
audio_time_base: Option<AVRational>,
|
||||
}
|
||||
|
||||
impl Muxer {
|
||||
pub unsafe fn new(path: &str, codec_ctx: *mut AVCodecContext, audio_codec_ctx: Option<*mut AVCodecContext>) -> Result<Self, String> {
|
||||
/// # Safety
|
||||
/// `codec_ctx` must be a valid, initialized pointer to an `AVCodecContext`.
|
||||
/// `audio_codec_ctx`, if provided, must also be a valid, initialized pointer.
|
||||
pub unsafe fn new(
|
||||
path: &str,
|
||||
codec_ctx: *mut AVCodecContext,
|
||||
audio_codec_ctx: Option<*mut AVCodecContext>,
|
||||
) -> Result<Self, String> {
|
||||
unsafe {
|
||||
let path_cstr = CString::new(path).unwrap();
|
||||
let path_cstr = CString::new(path).map_err(|e| e.to_string())?;
|
||||
let mut fmt_ctx: *mut AVFormatContext = std::ptr::null_mut();
|
||||
|
||||
let ret = avformat_alloc_output_context2(&mut fmt_ctx, std::ptr::null(), std::ptr::null(), path_cstr.as_ptr());
|
||||
if ret < 0 {
|
||||
|
||||
let ret = avformat_alloc_output_context2(
|
||||
&mut fmt_ctx,
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
path_cstr.as_ptr(),
|
||||
);
|
||||
if ret < 0 || fmt_ctx.is_null() {
|
||||
return Err("Failed to alloc output context".into());
|
||||
}
|
||||
|
||||
|
||||
let stream = avformat_new_stream(fmt_ctx, std::ptr::null());
|
||||
if stream.is_null() {
|
||||
return Err("Failed to create stream".into());
|
||||
avformat_free_context(fmt_ctx);
|
||||
return Err("Failed to create video stream".into());
|
||||
}
|
||||
|
||||
|
||||
(*stream).id = 0;
|
||||
let ret = avcodec_parameters_from_context((*stream).codecpar, codec_ctx);
|
||||
if ret < 0 {
|
||||
avformat_free_context(fmt_ctx);
|
||||
return Err("Failed to copy video codec parameters".into());
|
||||
}
|
||||
(*stream).time_base = (*codec_ctx).time_base;
|
||||
|
||||
let video_time_base = (*codec_ctx).time_base;
|
||||
(*stream).time_base = video_time_base;
|
||||
|
||||
let mut audio_time_base = None;
|
||||
if let Some(actx) = audio_codec_ctx {
|
||||
let astream = avformat_new_stream(fmt_ctx, std::ptr::null());
|
||||
if !astream.is_null() {
|
||||
(*astream).id = 1;
|
||||
if avcodec_parameters_from_context((*astream).codecpar, actx) < 0 {
|
||||
eprintln!("Failed to copy audio codec parameters");
|
||||
eprintln!("Warning: Failed to copy audio codec parameters");
|
||||
}
|
||||
(*astream).time_base = (*actx).time_base;
|
||||
let atb = (*actx).time_base;
|
||||
(*astream).time_base = atb;
|
||||
audio_time_base = Some(atb);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ((*(*fmt_ctx).oformat).flags & AVFMT_NOFILE) == 0 {
|
||||
let ret = avio_open(&mut (*fmt_ctx).pb, path_cstr.as_ptr(), AVIO_FLAG_WRITE);
|
||||
if ret < 0 {
|
||||
return Err("Failed to open file".into());
|
||||
avformat_free_context(fmt_ctx);
|
||||
return Err("Failed to open output file".into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let ret = avformat_write_header(fmt_ctx, std::ptr::null_mut());
|
||||
if ret < 0 {
|
||||
if ((*(*fmt_ctx).oformat).flags & AVFMT_NOFILE) == 0 {
|
||||
avio_closep(&mut (*fmt_ctx).pb);
|
||||
}
|
||||
avformat_free_context(fmt_ctx);
|
||||
return Err("Failed to write header".into());
|
||||
}
|
||||
|
||||
Ok(Self { fmt_ctx })
|
||||
|
||||
Ok(Self {
|
||||
fmt_ctx,
|
||||
video_time_base,
|
||||
audio_time_base,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn write_packet(&mut self, packet: &Packet) -> Result<(), String> {
|
||||
unsafe {
|
||||
// we must not pass the exact ptr to av_interleaved_write_frame directly
|
||||
// if we want to keep the packet, but since it's just writing, we can clone it
|
||||
if packet.ptr.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut new_pkt = av_packet_alloc();
|
||||
if new_pkt.is_null() {
|
||||
return Err("Failed to allocate packet".into());
|
||||
}
|
||||
av_packet_ref(new_pkt, packet.ptr);
|
||||
|
||||
let stream_idx = (*new_pkt).stream_index;
|
||||
if stream_idx >= 0 && (stream_idx as u32) < (*self.fmt_ctx).nb_streams {
|
||||
let out_stream = *(*self.fmt_ctx).streams.add(stream_idx as usize);
|
||||
let in_tb = if stream_idx == 0 {
|
||||
self.video_time_base
|
||||
} else {
|
||||
self.audio_time_base.unwrap_or(self.video_time_base)
|
||||
};
|
||||
av_packet_rescale_ts(new_pkt, in_tb, (*out_stream).time_base);
|
||||
}
|
||||
|
||||
let ret = av_interleaved_write_frame(self.fmt_ctx, new_pkt);
|
||||
av_packet_free(&mut new_pkt); // Wait, av_interleaved takes ownership usually?
|
||||
// Actually av_interleaved_write_frame takes ownership of the packet reference,
|
||||
// but we can just use av_write_frame if we don't want it buffered,
|
||||
// but av_interleaved_write_frame will unref the packet for us!
|
||||
// Wait, if it unrefs it, we shouldn't free it again.
|
||||
av_packet_free(&mut new_pkt);
|
||||
|
||||
if ret < 0 {
|
||||
return Err("Failed to write frame".into());
|
||||
return Err(format!("Failed to write frame: {}", ret));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn finalize(&mut self) -> Result<(), String> {
|
||||
unsafe {
|
||||
av_write_trailer(self.fmt_ctx);
|
||||
let ret = av_write_trailer(self.fmt_ctx);
|
||||
if ret < 0 {
|
||||
return Err(format!("Failed to write trailer: {}", ret));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -86,10 +132,13 @@ impl Muxer {
|
|||
impl Drop for Muxer {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if ((*(*self.fmt_ctx).oformat).flags & AVFMT_NOFILE) == 0 {
|
||||
avio_closep(&mut (*self.fmt_ctx).pb);
|
||||
if !self.fmt_ctx.is_null() {
|
||||
if !(*self.fmt_ctx).pb.is_null() && ((*(*self.fmt_ctx).oformat).flags & AVFMT_NOFILE) == 0 {
|
||||
avio_closep(&mut (*self.fmt_ctx).pb);
|
||||
}
|
||||
avformat_free_context(self.fmt_ctx);
|
||||
self.fmt_ctx = std::ptr::null_mut();
|
||||
}
|
||||
avformat_free_context(self.fmt_ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,13 +141,22 @@ pub fn show_menu_overlay() {
|
|||
window_clone.close();
|
||||
});
|
||||
|
||||
// Toggle buttons logic (just UI for now, backend IPC will be needed to actually toggle daemon state)
|
||||
let _btn_cfg_1 = config.clone();
|
||||
let window_esc = window.clone();
|
||||
window.connect_key_press_event(move |_, key| {
|
||||
if key.keyval() == gdk::keys::constants::Escape {
|
||||
window_esc.close();
|
||||
gtk::glib::Propagation::Stop
|
||||
} else {
|
||||
gtk::glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
|
||||
let replay_btn_clone = replay_btn.clone();
|
||||
replay_btn.connect_clicked(move |_| {
|
||||
let mut cfg = VrecConfig::load();
|
||||
cfg.replay_enabled = !cfg.replay_enabled;
|
||||
cfg.save().unwrap(); VrecConfig::notify_daemon_reload();
|
||||
let _ = cfg.save();
|
||||
VrecConfig::notify_daemon_reload();
|
||||
if cfg.replay_enabled {
|
||||
replay_btn_clone.set_label("Instant Replay\n🟢 ON");
|
||||
} else {
|
||||
|
|
@ -155,12 +164,11 @@ pub fn show_menu_overlay() {
|
|||
}
|
||||
});
|
||||
|
||||
let _btn_cfg_2 = config.clone();
|
||||
let record_btn_clone = record_btn.clone();
|
||||
record_btn.connect_clicked(move |_| {
|
||||
let mut cfg = VrecConfig::load();
|
||||
cfg.record_enabled = !cfg.record_enabled;
|
||||
cfg.save().unwrap();
|
||||
let _ = cfg.save();
|
||||
VrecConfig::notify_daemon_reload();
|
||||
if cfg.record_enabled {
|
||||
record_btn_clone.set_label("Recording\n🔴 ON");
|
||||
|
|
|
|||
|
|
@ -5,16 +5,18 @@ use crate::config::VrecConfig;
|
|||
pub fn open_replay_settings(config_in: &VrecConfig) {
|
||||
let window = Window::new(WindowType::Toplevel);
|
||||
window.set_title("Replay Settings");
|
||||
window.set_default_size(300, 200);
|
||||
window.set_default_size(320, 200);
|
||||
|
||||
let vbox = Box::new(Orientation::Vertical, 10);
|
||||
vbox.set_margin(15);
|
||||
|
||||
let duration_label = Label::new(Some("Replay Duration (seconds):"));
|
||||
duration_label.set_halign(gtk::Align::Start);
|
||||
let duration_entry = Entry::new();
|
||||
duration_entry.set_text(&config_in.replay_duration_sec.to_string());
|
||||
|
||||
let bitrate_label = Label::new(Some("Bitrate (kbps):"));
|
||||
bitrate_label.set_halign(gtk::Align::Start);
|
||||
let bitrate_entry = Entry::new();
|
||||
bitrate_entry.set_text(&config_in.replay_bitrate_kbps.to_string());
|
||||
|
||||
|
|
@ -37,22 +39,34 @@ pub fn open_replay_settings(config_in: &VrecConfig) {
|
|||
if let Ok(val) = bitrate_entry.text().parse() {
|
||||
cfg.replay_bitrate_kbps = val;
|
||||
}
|
||||
let _ = cfg.save(); VrecConfig::notify_daemon_reload();
|
||||
let _ = cfg.save();
|
||||
VrecConfig::notify_daemon_reload();
|
||||
window_clone.close();
|
||||
});
|
||||
|
||||
let window_esc = window.clone();
|
||||
window.connect_key_press_event(move |_, key| {
|
||||
if key.keyval() == gdk::keys::constants::Escape {
|
||||
window_esc.close();
|
||||
gtk::glib::Propagation::Stop
|
||||
} else {
|
||||
gtk::glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
|
||||
window.show_all();
|
||||
}
|
||||
|
||||
pub fn open_record_settings(config_in: &VrecConfig) {
|
||||
let window = Window::new(WindowType::Toplevel);
|
||||
window.set_title("Record Settings");
|
||||
window.set_default_size(300, 150);
|
||||
window.set_default_size(320, 150);
|
||||
|
||||
let vbox = Box::new(Orientation::Vertical, 10);
|
||||
vbox.set_margin(15);
|
||||
|
||||
let bitrate_label = Label::new(Some("Recording Bitrate (kbps):"));
|
||||
bitrate_label.set_halign(gtk::Align::Start);
|
||||
let bitrate_entry = Entry::new();
|
||||
bitrate_entry.set_text(&config_in.record_bitrate_kbps.to_string());
|
||||
|
||||
|
|
@ -70,17 +84,28 @@ pub fn open_record_settings(config_in: &VrecConfig) {
|
|||
if let Ok(val) = bitrate_entry.text().parse() {
|
||||
cfg.record_bitrate_kbps = val;
|
||||
}
|
||||
let _ = cfg.save(); VrecConfig::notify_daemon_reload();
|
||||
let _ = cfg.save();
|
||||
VrecConfig::notify_daemon_reload();
|
||||
window_clone.close();
|
||||
});
|
||||
|
||||
let window_esc = window.clone();
|
||||
window.connect_key_press_event(move |_, key| {
|
||||
if key.keyval() == gdk::keys::constants::Escape {
|
||||
window_esc.close();
|
||||
gtk::glib::Propagation::Stop
|
||||
} else {
|
||||
gtk::glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
|
||||
window.show_all();
|
||||
}
|
||||
|
||||
pub fn open_general_settings(config_in: &VrecConfig) {
|
||||
let window = Window::new(WindowType::Toplevel);
|
||||
window.set_title("General Settings");
|
||||
window.set_default_size(350, 300);
|
||||
window.set_default_size(380, 320);
|
||||
|
||||
let vbox = Box::new(Orientation::Vertical, 10);
|
||||
vbox.set_margin(15);
|
||||
|
|
@ -95,6 +120,7 @@ pub fn open_general_settings(config_in: &VrecConfig) {
|
|||
|
||||
// Theme
|
||||
let theme_label = Label::new(Some("UI Theme:"));
|
||||
theme_label.set_halign(gtk::Align::Start);
|
||||
let theme_combo = ComboBoxText::new();
|
||||
theme_combo.append_text("dark");
|
||||
theme_combo.append_text("light");
|
||||
|
|
@ -102,15 +128,18 @@ pub fn open_general_settings(config_in: &VrecConfig) {
|
|||
|
||||
// Language
|
||||
let lang_label = Label::new(Some("Language Code (e.g. en, fr):"));
|
||||
lang_label.set_halign(gtk::Align::Start);
|
||||
let lang_entry = Entry::new();
|
||||
lang_entry.set_text(&config_in.language);
|
||||
|
||||
// Hotkeys
|
||||
let menu_hk_label = Label::new(Some("Menu Hotkey:"));
|
||||
menu_hk_label.set_halign(gtk::Align::Start);
|
||||
let menu_hk_entry = Entry::new();
|
||||
menu_hk_entry.set_text(&config_in.menu_hotkey);
|
||||
|
||||
let save_hk_label = Label::new(Some("Save Replay Hotkey:"));
|
||||
save_hk_label.set_halign(gtk::Align::Start);
|
||||
let save_hk_entry = Entry::new();
|
||||
save_hk_entry.set_text(&config_in.save_hotkey);
|
||||
|
||||
|
|
@ -139,9 +168,20 @@ pub fn open_general_settings(config_in: &VrecConfig) {
|
|||
cfg.language = lang_entry.text().to_string();
|
||||
cfg.menu_hotkey = menu_hk_entry.text().to_string();
|
||||
cfg.save_hotkey = save_hk_entry.text().to_string();
|
||||
let _ = cfg.save(); VrecConfig::notify_daemon_reload();
|
||||
let _ = cfg.save();
|
||||
VrecConfig::notify_daemon_reload();
|
||||
window_clone.close();
|
||||
});
|
||||
|
||||
let window_esc = window.clone();
|
||||
window.connect_key_press_event(move |_, key| {
|
||||
if key.keyval() == gdk::keys::constants::Escape {
|
||||
window_esc.close();
|
||||
gtk::glib::Propagation::Stop
|
||||
} else {
|
||||
gtk::glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
|
||||
window.show_all();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue