diff --git a/Cargo.lock b/Cargo.lock index 6125e44..0de94cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index baf8669..5793a9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/scratch/enc.patch b/scratch/enc.patch new file mode 100644 index 0000000..78849f5 --- /dev/null +++ b/scratch/enc.patch @@ -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); + } + } diff --git a/scratch/enc_mod.patch b/scratch/enc_mod.patch new file mode 100644 index 0000000..8f5ec39 --- /dev/null +++ b/scratch/enc_mod.patch @@ -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, 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) + } + } diff --git a/scratch/test_ring.rs b/scratch/test_ring.rs new file mode 100644 index 0000000..e68dee2 --- /dev/null +++ b/scratch/test_ring.rs @@ -0,0 +1,5 @@ +use ringbuf::HeapRb; +fn main() { + let rb = HeapRb::::new(10); + let (mut prod, mut cons) = rb.split(); +} diff --git a/src/capture/mod.rs b/src/capture/mod.rs index 635203b..a9111c1 100644 --- a/src/capture/mod.rs +++ b/src/capture/mod.rs @@ -23,6 +23,6 @@ pub enum Frame { }, } -pub trait FrameSource { - fn next_frame(&mut self) -> Result>; +pub trait FrameSource: Send { + fn next_frame(&mut self) -> Result>; } diff --git a/src/capture/wayland.rs b/src/capture/wayland.rs index 78c6892..50c0f5e 100644 --- a/src/capture/wayland.rs +++ b/src/capture/wayland.rs @@ -9,7 +9,7 @@ pub struct WaylandCapture { } impl WaylandCapture { - pub async fn new() -> Result> { + pub async fn new() -> Result> { 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> { + fn next_frame(&mut self) -> Result> { Err("Wayland/Pipewire frame extraction not fully implemented yet".into()) } } diff --git a/src/capture/x11.rs b/src/capture/x11.rs index b1b0cb5..30b7b28 100644 --- a/src/capture/x11.rs +++ b/src/capture/x11.rs @@ -9,7 +9,7 @@ pub struct X11Capture { } impl X11Capture { - pub fn new() -> Result> { + pub fn new() -> Result> { 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> { + fn next_frame(&mut self) -> Result> { let cookie = self.conn.send_request(&x::GetImage { format: x::ImageFormat::ZPixmap, drawable: x::Drawable::Window(self.root), diff --git a/src/debug.rs b/src/debug.rs index 26700dd..9c81f49 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -5,7 +5,7 @@ pub struct DebugWindow { } impl DebugWindow { - pub fn new(width: usize, height: usize) -> Result> { + pub fn new(width: usize, height: usize) -> Result> { 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> { + pub fn update(&mut self, buffer: &[u32], width: usize, height: usize) -> Result<(), Box> { self.window.update_with_buffer(buffer, width, height)?; Ok(()) } diff --git a/src/encoder.rs b/src/encoder.rs index 5e098e9..96974c4 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -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, 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) } } diff --git a/src/hotkey.rs b/src/hotkey.rs new file mode 100644 index 0000000..febde6b --- /dev/null +++ b/src/hotkey.rs @@ -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> { + 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(()); + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 03434a5..9f1aedf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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> { +async fn main() -> Result<(), Box> { 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> { Box::new(capture::x11::X11Capture::new()?) }; - let mut window: Option = 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::(5); + let (trigger_tx, trigger_rx) = bounded::<()>(1); + let (mux_tx, mux_rx) = bounded::>(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 = 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::::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::>(); + 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(()) diff --git a/src/muxer.rs b/src/muxer.rs new file mode 100644 index 0000000..a13754b --- /dev/null +++ b/src/muxer.rs @@ -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 { + 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); + } + } +} diff --git a/src/ring.rs b/src/ring.rs new file mode 100644 index 0000000..9a4eea0 --- /dev/null +++ b/src/ring.rs @@ -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 } + } + } +} +