Phase 3: Implement Ring Buffer, muxer logic, and hotkey threading

This commit is contained in:
katR 2026-09-01 19:48:20 +05:30
parent 4c491eea0b
commit 2c1a464c03
14 changed files with 760 additions and 46 deletions

362
Cargo.lock generated
View file

@ -112,6 +112,9 @@ name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
dependencies = [
"serde_core",
]
[[package]]
name = "bumpalo"
@ -119,6 +122,12 @@ version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.12.1"
@ -187,6 +196,31 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2"
[[package]]
name = "crossbeam-channel"
version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
[[package]]
name = "dispatch2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags 2.13.1",
"objc2",
]
[[package]]
name = "dlib"
version = "0.5.3"
@ -248,7 +282,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@ -409,6 +443,16 @@ dependencies = [
"slab",
]
[[package]]
name = "gethostname"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
"rustix",
"windows-link",
]
[[package]]
name = "getrandom"
version = "0.4.3"
@ -426,6 +470,23 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
[[package]]
name = "global-hotkey"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c386b0a4a70cb2d39fffd74480f985b6f0bfbcb934b6a6b6b7e630e448f242e"
dependencies = [
"crossbeam-channel",
"keyboard-types",
"objc2",
"objc2-app-kit",
"once_cell",
"thiserror 2.0.20",
"windows-sys 0.59.0",
"x11rb",
"xkeysym",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
@ -498,6 +559,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "keyboard-types"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
dependencies = [
"bitflags 2.13.1",
"serde",
"unicode-segmentation",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@ -641,7 +713,21 @@ checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
dependencies = [
"libc",
"wasi",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "mp4"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9ef834d5ed55e494a2ae350220314dc4aacd1c43a9498b00e320e0ea352a5c3"
dependencies = [
"byteorder",
"bytes",
"num-rational",
"serde",
"serde_json",
"thiserror 1.0.69",
]
[[package]]
@ -675,6 +761,46 @@ dependencies = [
"memchr",
]
[[package]]
name = "num-bigint"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
"serde",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.17.0"
@ -685,6 +811,54 @@ dependencies = [
"libc",
]
[[package]]
name = "objc2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-app-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags 2.13.1",
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.13.1",
"dispatch2",
"objc2",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "objc2-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.1",
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@ -761,6 +935,21 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "portable-atomic"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "portable-atomic-util"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
@ -847,6 +1036,17 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "ringbuf"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a158e09ede21a14b172ca6cdd6208386c6ae2cb6acef58d774368ef8c450dfa7"
dependencies = [
"crossbeam-utils",
"portable-atomic",
"portable-atomic-util",
]
[[package]]
name = "rustc-hash"
version = "2.1.3"
@ -863,7 +1063,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@ -1005,7 +1205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@ -1059,7 +1259,47 @@ dependencies = [
"getrandom",
"once_cell",
"rustix",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl 2.0.20",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "thiserror-impl"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
@ -1076,7 +1316,7 @@ dependencies = [
"socket2",
"tokio-macros",
"tracing",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@ -1180,7 +1420,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset 0.9.1",
"tempfile",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@ -1189,6 +1429,12 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unicode-width"
version = "0.2.2"
@ -1229,12 +1475,16 @@ name = "vrec"
version = "0.1.0"
dependencies = [
"ashpd",
"crossbeam-channel",
"ffmpeg-next",
"ffmpeg-sys-next",
"global-hotkey",
"libc",
"memmap2",
"minifb",
"mp4",
"pipewire",
"ringbuf",
"tokio",
"xcb",
]
@ -1413,6 +1663,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@ -1422,6 +1681,70 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "1.0.4"
@ -1442,6 +1765,23 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "x11rb"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname",
"rustix",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "xcb"
version = "1.7.1"
@ -1459,6 +1799,12 @@ version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23"
[[package]]
name = "xkeysym"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
name = "xml-rs"
version = "0.8.29"
@ -1488,7 +1834,7 @@ dependencies = [
"tracing",
"uds_windows",
"uuid",
"windows-sys",
"windows-sys 0.61.2",
"winnow",
"zbus_macros",
"zbus_names",

View file

@ -5,11 +5,15 @@ edition = "2024"
[dependencies]
ashpd = { version = "0.13.13", features = ["screencast"] }
crossbeam-channel = "0.5.16"
ffmpeg-next = "9.0.0"
ffmpeg-sys-next = "9.0.0"
global-hotkey = "0.8.0"
libc = "0.2.189"
memmap2 = "0.9.11"
minifb = "0.28.0"
mp4 = "0.14.0"
pipewire = "0.10.1"
ringbuf = "0.5.1"
tokio = { version = "1.53.1", features = ["rt", "macros", "rt-multi-thread"] }
xcb = { version = "1.7.1", features = ["shm", "xfixes"] }

99
scratch/enc.patch Normal file
View file

@ -0,0 +1,99 @@
--- src/encoder.rs
+++ src/encoder.rs
@@ -6,6 +6,7 @@
codec_ctx: *mut AVCodecContext,
hw_device_ctx: *mut AVBufferRef,
hw_frames_ctx: *mut AVBufferRef,
+ next_pts: i64,
}
impl VaapiEncoder {
@@ -70,6 +71,7 @@
codec_ctx,
hw_device_ctx,
hw_frames_ctx: hw_frames_ref,
+ next_pts: 0,
})
}
}
@@ -77,15 +79,63 @@
pub fn encode_frame(&mut self, frame: &Frame) -> Result<(), String> {
unsafe {
match frame {
- Frame::Raw { width, height, stride, data } => {
- // For Phase 2 X11 fallback:
- // 1. Convert BGRA to NV12 (omitted/mocked for brevity if not using swscale yet)
- // 2. Upload NV12 to a VAAPI AVFrame via av_hwframe_get_buffer and av_hwframe_transfer_data
- // 3. Send to avcodec_send_frame
- println!("VAAPI: Mock encoding Raw CPU frame (X11 fallback). Needs BGRA->NV12 swscale.");
- }
- Frame::DmaBuf { width, height, format, fd, stride, offset } => {
- // For Phase 2 Wayland zero-copy:
- // 1. Populate AVDRMFrameDescriptor with the FD and modifier.
- // 2. Create an AVFrame with format AV_PIX_FMT_DRM_PRIME.
- // 3. Use av_hwframe_map to map it to AV_PIX_FMT_VAAPI.
- // 4. Send to avcodec_send_frame.
- println!("VAAPI: Mock encoding DMA-BUF frame (Wayland zero-copy). FD: {}", fd);
+ Frame::Raw { width, height, stride: in_stride, data } => {
+ let mut bgra_frame = av_frame_alloc();
+ (*bgra_frame).format = AVPixelFormat::AV_PIX_FMT_BGRA as i32;
+ (*bgra_frame).width = *width as i32;
+ (*bgra_frame).height = *height as i32;
+ av_frame_get_buffer(bgra_frame, 32);
+
+ let bgra_stride = (*bgra_frame).linesize[0] as usize;
+ for y in 0..(*height as usize) {
+ let src_row = &data[y * (*in_stride as usize) .. y * (*in_stride as usize) + (*width as usize) * 4];
+ let dst_row = std::slice::from_raw_parts_mut(
+ (*bgra_frame).data[0].add(y * bgra_stride),
+ (*width as usize) * 4
+ );
+ dst_row.copy_from_slice(src_row);
+ }
+
+ let mut nv12_frame = av_frame_alloc();
+ (*nv12_frame).format = AVPixelFormat::AV_PIX_FMT_NV12 as i32;
+ (*nv12_frame).width = *width as i32;
+ (*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,
+ SWS_BILINEAR, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()
+ );
+
+ sws_scale(
+ sws_ctx,
+ (*bgra_frame).data.as_ptr() as *const *const u8,
+ (*bgra_frame).linesize.as_ptr(),
+ 0,
+ *height as i32,
+ (*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);
+ av_hwframe_transfer_data(hw_frame, nv12_frame, 0);
+
+ (*hw_frame).pts = self.next_pts;
+ self.next_pts += 1;
+
+ if avcodec_send_frame(self.codec_ctx, hw_frame) >= 0 {
+ let mut pkt = av_packet_alloc();
+ while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
+ println!("VAAPI: encoded packet of size {}", (*pkt).size);
+ av_packet_unref(pkt);
+ }
+ av_packet_free(&mut pkt);
+ }
+ av_frame_free(&mut hw_frame);
+ av_frame_free(&mut nv12_frame);
+ av_frame_free(&mut bgra_frame);
+ }
+ Frame::DmaBuf { width: _, height: _, format: _, fd, stride: _, offset: _ } => {
+ println!("VAAPI: Mock encoding DMA-BUF frame (Wayland zero-copy). FD: {}", fd);
}
}

42
scratch/enc_mod.patch Normal file
View file

@ -0,0 +1,42 @@
--- src/encoder.rs
+++ src/encoder.rs
@@ -2,7 +2,7 @@
use std::ptr;
use crate::capture::Frame;
+use crate::ring::Packet;
pub struct VaapiEncoder {
codec_ctx: *mut AVCodecContext,
@@ -62,6 +62,10 @@
}
}
- pub fn encode_frame(&mut self, frame: &Frame) -> Result<(), String> {
+ pub fn codec_ctx(&self) -> *mut AVCodecContext {
+ self.codec_ctx
+ }
+
+ pub fn encode_frame(&mut self, frame: &Frame) -> Result<Vec<Packet>, String> {
+ let mut packets = Vec::new();
unsafe {
match frame {
@@ -107,8 +111,8 @@
if avcodec_send_frame(self.codec_ctx, hw_frame) >= 0 {
let mut pkt = av_packet_alloc();
while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
- println!("VAAPI: encoded packet of size {}", (*pkt).size);
- av_packet_unref(pkt);
+ let new_pkt = av_packet_alloc();
+ av_packet_move_ref(new_pkt, pkt);
+ packets.push(Packet::new(new_pkt));
}
av_packet_free(&mut pkt);
}
@@ -122,7 +126,7 @@
}
}
}
- Ok(())
+ Ok(packets)
}
}

5
scratch/test_ring.rs Normal file
View file

@ -0,0 +1,5 @@
use ringbuf::HeapRb;
fn main() {
let rb = HeapRb::<i32>::new(10);
let (mut prod, mut cons) = rb.split();
}

View file

@ -23,6 +23,6 @@ pub enum Frame {
},
}
pub trait FrameSource {
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error>>;
pub trait FrameSource: Send {
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>>;
}

View file

@ -9,7 +9,7 @@ pub struct WaylandCapture {
}
impl WaylandCapture {
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
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?;
@ -36,7 +36,7 @@ impl WaylandCapture {
}
impl FrameSource for WaylandCapture {
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error>> {
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
Err("Wayland/Pipewire frame extraction not fully implemented yet".into())
}
}

View file

@ -9,7 +9,7 @@ pub struct X11Capture {
}
impl X11Capture {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
pub fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let (conn, screen_num) = Connection::connect(None)?;
let setup = conn.get_setup();
let screen = setup.roots().nth(screen_num as usize).ok_or("No screen found")?;
@ -24,7 +24,7 @@ impl X11Capture {
}
impl FrameSource for X11Capture {
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error>> {
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
let cookie = self.conn.send_request(&x::GetImage {
format: x::ImageFormat::ZPixmap,
drawable: x::Drawable::Window(self.root),

View file

@ -5,7 +5,7 @@ pub struct DebugWindow {
}
impl DebugWindow {
pub fn new(width: usize, height: usize) -> Result<Self, Box<dyn std::error::Error>> {
pub fn new(width: usize, height: usize) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let window = Window::new(
"vrec debug capture",
width,
@ -15,7 +15,7 @@ impl DebugWindow {
Ok(Self { window })
}
pub fn update(&mut self, buffer: &[u32], width: usize, height: usize) -> Result<(), Box<dyn std::error::Error>> {
pub fn update(&mut self, buffer: &[u32], width: usize, height: usize) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.window.update_with_buffer(buffer, width, height)?;
Ok(())
}

View file

@ -72,7 +72,12 @@ impl VaapiEncoder {
}
}
pub fn encode_frame(&mut self, frame: &Frame) -> Result<(), String> {
pub fn codec_ctx(&self) -> *mut AVCodecContext {
self.codec_ctx
}
pub fn encode_frame(&mut self, frame: &Frame) -> Result<Vec<crate::ring::Packet>, String> {
let mut packets = Vec::new();
unsafe {
match frame {
Frame::Raw { width, height, stride: in_stride, data } => {
@ -125,8 +130,9 @@ impl VaapiEncoder {
if avcodec_send_frame(self.codec_ctx, hw_frame) >= 0 {
let mut pkt = av_packet_alloc();
while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
println!("VAAPI: encoded packet of size {}", (*pkt).size);
av_packet_unref(pkt);
let new_pkt = av_packet_alloc();
av_packet_move_ref(new_pkt, pkt);
packets.push(crate::ring::Packet::new(new_pkt));
}
av_packet_free(&mut pkt);
}
@ -139,7 +145,7 @@ impl VaapiEncoder {
}
}
}
Ok(())
Ok(packets)
}
}

22
src/hotkey.rs Normal file
View file

@ -0,0 +1,22 @@
use global_hotkey::{GlobalHotKeyManager, hotkey::{HotKey, Modifiers, Code}, GlobalHotKeyEvent};
use crossbeam_channel::Sender;
pub fn run_hotkey_listener(trigger_tx: Sender<()>) -> Result<(), Box<dyn std::error::Error>> {
let manager = GlobalHotKeyManager::new()?;
let hotkey = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR);
manager.register(hotkey)?;
println!("Registered global hotkey Ctrl+Shift+R to trigger replay save.");
let receiver = GlobalHotKeyEvent::receiver();
loop {
if let Ok(event) = receiver.recv() {
if event.id == hotkey.id() {
if event.state == global_hotkey::HotKeyState::Pressed {
println!("Hotkey triggered! Saving replay...");
let _ = trigger_tx.send(());
}
}
}
}
}

View file

@ -1,13 +1,22 @@
pub mod capture;
pub mod encoder;
pub mod ring;
pub mod muxer;
pub mod hotkey;
pub mod debug;
use capture::{Frame, FrameSource};
use debug::DebugWindow;
use encoder::VaapiEncoder;
use ring::Packet;
use muxer::Muxer;
use ringbuf::HeapRb;
use ringbuf::traits::{RingBuffer, Consumer};
use crossbeam_channel::bounded;
use std::thread;
use std::env;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string());
println!("Detected session type: {}", session_type);
@ -19,39 +28,93 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Box::new(capture::x11::X11Capture::new()?)
};
let mut window: Option<DebugWindow> = None;
let first_frame = source.next_frame()?;
let (width, height) = match &first_frame {
Frame::Raw { width, height, .. } => (*width, *height),
Frame::DmaBuf { width, height, .. } => (*width, *height),
};
println!("Starting capture loop...");
loop {
match source.next_frame() {
Ok(Frame::Raw { width, height, data, stride }) => {
if window.is_none() {
window = Some(DebugWindow::new(width as usize, height as usize)?);
}
// 1. Channels
let (frame_tx, frame_rx) = bounded::<Frame>(5);
let (trigger_tx, trigger_rx) = bounded::<()>(1);
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(1);
// Send first frame to encoder thread
let _ = frame_tx.send(first_frame);
if let Some(win) = &mut window {
if !win.is_open() {
// Capture thread
let capture_tx = frame_tx.clone();
thread::spawn(move || {
loop {
match source.next_frame() {
Ok(frame) => {
if capture_tx.send(frame).is_err() {
break;
}
// For minifb, we need to cast &[u8] to &[u32].
// Assuming BGRA or similar 4-byte format.
let u32_data: Vec<u32> = data
.chunks_exact(4)
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
win.update(&u32_data, width as usize, height as usize)?;
}
Err(e) => {
eprintln!("Capture error: {}", e);
break;
}
}
Ok(Frame::DmaBuf { .. }) => {
println!("Wayland/PipeWire DMA-BUF received. Phase 1 debug viewer not fully implemented for DMA-BUFs yet.");
break;
}
});
// 3. Encoder Thread
thread::spawn(move || {
let mut encoder = VaapiEncoder::new(width, height).expect("Failed to init encoder");
let codec_ctx_ptr = encoder.codec_ctx() as usize; // Send raw pointer safely
let mut ring = HeapRb::<Packet>::new(3600); // 60 seconds at 60fps
// Muxer Thread spawned inside here so it shares lifetime conceptually,
// though we use mux_rx.
thread::spawn(move || {
while let Ok(mut drain) = mux_rx.recv() {
println!("Saving replay to output.mp4...");
let mut start_idx = 0;
for (i, p) in drain.iter().enumerate() {
if p.is_keyframe() {
start_idx = i;
break;
}
}
if start_idx < drain.len() {
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
let mut muxer = Muxer::new("output.mp4", codec_ctx).unwrap();
for p in drain.into_iter().skip(start_idx) {
let _ = muxer.write_packet(&p);
}
let _ = muxer.finalize();
println!("Replay saved!");
} else {
println!("No keyframe found in buffer, skipping save.");
}
}
Err(e) => {
eprintln!("Capture error: {}", e);
break;
});
// Encoder loop
while let Ok(frame) = frame_rx.recv() {
if let Ok(packets) = encoder.encode_frame(&frame) {
for pkt in packets {
ring.push_overwrite(pkt);
}
}
// Check for trigger
if trigger_rx.try_recv().is_ok() {
// Clone all packets to send to muxer
let drain = ring.iter().cloned().collect::<Vec<_>>();
let _ = mux_tx.try_send(drain);
}
}
});
// 5. Hotkey listener
println!("Starting hotkey listener...");
if let Err(e) = hotkey::run_hotkey_listener(trigger_tx) {
eprintln!("Failed to start hotkey listener: {}", e);
}
Ok(())

82
src/muxer.rs Normal file
View file

@ -0,0 +1,82 @@
use ffmpeg_next::ffi::*;
use crate::ring::Packet;
use std::ffi::CString;
pub struct Muxer {
fmt_ctx: *mut AVFormatContext,
}
impl Muxer {
pub fn new(path: &str, codec_ctx: *mut AVCodecContext) -> Result<Self, String> {
unsafe {
let path_cstr = CString::new(path).unwrap();
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 {
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());
}
let ret = avcodec_parameters_from_context((*stream).codecpar, codec_ctx);
if ret < 0 {
return Err("Failed to copy codec parameters".into());
}
if ((*(*fmt_ctx).oformat).flags & AVFMT_NOFILE as i32) == 0 {
let ret = avio_open(&mut (*fmt_ctx).pb, path_cstr.as_ptr(), AVIO_FLAG_WRITE as i32);
if ret < 0 {
return Err("Failed to open file".into());
}
}
let ret = avformat_write_header(fmt_ctx, std::ptr::null_mut());
if ret < 0 {
return Err("Failed to write header".into());
}
Ok(Self { fmt_ctx })
}
}
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
let mut new_pkt = av_packet_alloc();
av_packet_ref(new_pkt, packet.ptr);
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.
if ret < 0 {
return Err("Failed to write frame".into());
}
}
Ok(())
}
pub fn finalize(&mut self) -> Result<(), String> {
unsafe {
av_write_trailer(self.fmt_ctx);
}
Ok(())
}
}
impl Drop for Muxer {
fn drop(&mut self) {
unsafe {
if ((*(*self.fmt_ctx).oformat).flags & AVFMT_NOFILE as i32) == 0 {
avio_closep(&mut (*self.fmt_ctx).pb);
}
avformat_free_context(self.fmt_ctx);
}
}
}

45
src/ring.rs Normal file
View file

@ -0,0 +1,45 @@
use ringbuf::traits::{Producer, Consumer, Split};
use ringbuf::HeapRb;
use ffmpeg_next::ffi::*;
pub struct Packet {
pub ptr: *mut AVPacket,
}
unsafe impl Send for Packet {}
unsafe impl Sync for Packet {}
impl Packet {
pub fn new(ptr: *mut AVPacket) -> Self {
Self { ptr }
}
pub fn is_keyframe(&self) -> bool {
unsafe {
((*self.ptr).flags & AV_PKT_FLAG_KEY as i32) != 0
}
}
}
impl Drop for Packet {
fn drop(&mut self) {
unsafe {
if !self.ptr.is_null() {
let mut p = self.ptr;
av_packet_free(&mut p);
self.ptr = std::ptr::null_mut();
}
}
}
}
impl Clone for Packet {
fn clone(&self) -> Self {
unsafe {
let new_pkt = av_packet_alloc();
av_packet_ref(new_pkt, self.ptr);
Self { ptr: new_pkt }
}
}
}