├── .gitattributes ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── BUILDING.md ├── COPYING ├── README.md ├── icon.png ├── index.html ├── libs ├── hbb_common │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ ├── examples │ │ ├── config.rs │ │ └── system_message.rs │ ├── protos │ │ ├── message.proto │ │ └── rendezvous.proto │ └── src │ │ ├── bytes_codec.rs │ │ ├── compress.rs │ │ ├── config.rs │ │ ├── fs.rs │ │ ├── keyboard.rs │ │ ├── lib.rs │ │ ├── password_security.rs │ │ ├── platform │ │ ├── linux.rs │ │ ├── macos.rs │ │ ├── mod.rs │ │ └── windows.rs │ │ ├── protos │ │ └── mod.rs │ │ ├── proxy.rs │ │ ├── socket_client.rs │ │ ├── tcp.rs │ │ └── udp.rs ├── prebuilt │ └── windows │ │ ├── avcodec-60.dll │ │ ├── avfilter-9.dll │ │ ├── avformat-60.dll │ │ ├── avutil-58.dll │ │ ├── bz2.dll │ │ ├── ffi-7.dll │ │ ├── gio-2.0-0.dll │ │ ├── glib-2.0-0.dll │ │ ├── gmodule-2.0-0.dll │ │ ├── gobject-2.0-0.dll │ │ ├── gstapp-1.0-0.dll │ │ ├── gstapp.dll │ │ ├── gstaudio-1.0-0.dll │ │ ├── gstaudioconvert.dll │ │ ├── gstaudiomixer.dll │ │ ├── gstaudioparsers.dll │ │ ├── gstaudioresample.dll │ │ ├── gstbase-1.0-0.dll │ │ ├── gstcodecparsers-1.0-0.dll │ │ ├── gstcodecs-1.0-0.dll │ │ ├── gstcoreelements.dll │ │ ├── gstd3d11-1.0-0.dll │ │ ├── gstd3d11.dll │ │ ├── gstdxva-1.0-0.dll │ │ ├── gstid3demux.dll │ │ ├── gstisomp4.dll │ │ ├── gstlibav.dll │ │ ├── gstmatroska.dll │ │ ├── gstogg.dll │ │ ├── gstpbutils-1.0-0.dll │ │ ├── gstplayback.dll │ │ ├── gstpng.dll │ │ ├── gstrawparse.dll │ │ ├── gstreamer-1.0-0.dll │ │ ├── gstriff-1.0-0.dll │ │ ├── gstrtp-1.0-0.dll │ │ ├── gsttag-1.0-0.dll │ │ ├── gsttypefindfunctions.dll │ │ ├── gstvideo-1.0-0.dll │ │ ├── gstvideoconvertscale.dll │ │ ├── gstvideoparsersbad.dll │ │ ├── gstvorbis.dll │ │ ├── gstwavparse.dll │ │ ├── gstx264.dll │ │ ├── intl-8.dll │ │ ├── libFLAC-8.dll │ │ ├── libpng16-16.dll │ │ ├── libvorbis-0.dll │ │ ├── libvorbisenc-2.dll │ │ ├── libvorbisfile-3.dll │ │ ├── libx264-157.dll │ │ ├── msvcp140.dll │ │ ├── ogg-0.dll │ │ ├── orc-0.4-0.dll │ │ ├── pcre2-8-0.dll │ │ ├── swresample-4.dll │ │ ├── vcruntime140.dll │ │ ├── vcruntime140_1.dll │ │ └── z-1.dll └── scrap │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── examples │ ├── benchmark.rs │ ├── capture_mag.rs │ ├── ffplay.rs │ ├── list.rs │ ├── record-screen.rs │ └── screenshot.rs │ ├── screenshot0_1.png │ └── src │ ├── android │ ├── ffi.rs │ └── mod.rs │ ├── bindings │ ├── aom_ffi.h │ ├── vpx_ffi.h │ └── yuv_ffi.h │ ├── common │ ├── android.rs │ ├── aom.rs │ ├── codec.rs │ ├── convert.rs │ ├── dxgi.rs │ ├── hwcodec.rs │ ├── linux.rs │ ├── mediacodec.rs │ ├── mod.rs │ ├── quartz.rs │ ├── record.rs │ ├── vpx.rs │ ├── vpxcodec.rs │ ├── vram.rs │ ├── wayland.rs │ └── x11.rs │ ├── dxgi │ ├── gdi.rs │ ├── mag.rs │ └── mod.rs │ ├── lib.rs │ ├── quartz │ ├── capturer.rs │ ├── config.rs │ ├── display.rs │ ├── ffi.rs │ ├── frame.rs │ └── mod.rs │ ├── wayland.rs │ ├── wayland │ ├── README.md │ ├── capturable.rs │ ├── pipewire.rs │ ├── remote_desktop_portal.rs │ ├── request_portal.rs │ └── screencast_portal.rs │ └── x11 │ ├── capturer.rs │ ├── display.rs │ ├── ffi.rs │ ├── iter.rs │ ├── mod.rs │ └── server.rs ├── package-lock.json ├── package.json ├── postcss.config.js ├── record-control.html ├── src-tauri ├── .gitignore ├── Cargo.toml ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ ├── configuration.rs │ ├── main.rs │ ├── recorder.rs │ ├── transcriber.rs │ └── util.rs ├── tauri.conf.json └── tauri.windows.conf.json ├── src ├── App.tsx ├── RecordControl.tsx ├── assets │ ├── github-mark.svg │ ├── logo.png │ └── logo.svg ├── config.json ├── index.tsx ├── lang.json ├── record-control.tsx ├── styles.css └── util.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite-env.d.ts └── vite.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: "publish" 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | build-linux: 11 | name: "Build (Ubuntu latest)" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 22 18 | - uses: friendlyanon/setup-vcpkg@v1 19 | with: { committish: c8696863d371ab7f46e213d8f5ca923c4aef2a00 } 20 | - name: Install dependencies 21 | run: | 22 | sudo apt-get update 23 | sudo apt-get install libasound2-dev libudev-dev nasm libxcb-randr0-dev 24 | $VCPKG_ROOT/vcpkg install libvpx libyuv opus aom 25 | - name: Setup Tauri 26 | run: | 27 | sudo apt-get install -y build-essential libwebkit2gtk-4.0-dev curl wget file libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev 28 | - name: Install GStreamer 29 | uses: blinemedical/setup-gstreamer@v1.4.0 30 | with: 31 | version: "1.24" 32 | arch: "x86_64" 33 | - name: Install NPM dependencies 34 | run: npm install 35 | - name: Cache target 36 | uses: actions/cache@v3 37 | with: 38 | key: ${{ runner.OS }}-rust-cache-${{ hashFiles('**/Cargo.toml') }} 39 | path: | 40 | ~/.cargo/registry 41 | ~/.cargo/git 42 | src-tauri/target 43 | - name: Compile application 44 | run: npm run tauri build 45 | env: 46 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 47 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 48 | - uses: actions/upload-artifact@v4 49 | with: 50 | name: Release (Ubuntu latest) 51 | path: src-tauri/target/release/bundle/ 52 | if-no-files-found: "error" 53 | 54 | build-osx: 55 | # Intentionally disable the macOS build artifact 56 | if: false 57 | name: "Build (macOS latest)" 58 | runs-on: macos-latest 59 | steps: 60 | - uses: actions/checkout@v4 61 | - uses: actions/setup-node@v4 62 | with: 63 | node-version: 22 64 | - uses: friendlyanon/setup-vcpkg@v1 65 | with: { committish: c8696863d371ab7f46e213d8f5ca923c4aef2a00 } 66 | - name: Install dependencies 67 | run: | 68 | brew install nasm 69 | $VCPKG_ROOT/vcpkg install libvpx libyuv opus aom 70 | - name: Install GStreamer 71 | uses: blinemedical/setup-gstreamer@v1.4.0 72 | with: 73 | version: "1.24.5" 74 | - name: Install NPM dependencies 75 | run: npm install 76 | - name: Cache target 77 | uses: actions/cache@v3 78 | with: 79 | key: ${{ runner.OS }}-rust-cache-${{ hashFiles('**/Cargo.toml') }} 80 | path: | 81 | ~/.cargo/registry 82 | ~/.cargo/git 83 | src-tauri/target 84 | - name: Compile application 85 | run: npm run tauri build 86 | env: 87 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 88 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 89 | MACOSX_DEPLOYMENT_TARGET: "14.0" 90 | - uses: actions/upload-artifact@v4 91 | with: 92 | name: Release (macOS latest) 93 | path: src-tauri/target/release/bundle/ 94 | if-no-files-found: "error" 95 | 96 | build-windows: 97 | name: "Build (Windows latest)" 98 | runs-on: windows-latest 99 | steps: 100 | - uses: actions/checkout@v4 101 | - uses: actions/setup-node@v4 102 | with: 103 | node-version: 22 104 | - uses: friendlyanon/setup-vcpkg@v1 105 | with: { committish: c8696863d371ab7f46e213d8f5ca923c4aef2a00 } 106 | - name: Install dependencies 107 | run: | 108 | & "$env:VCPKG_ROOT\vcpkg" install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static 109 | - name: Install GStreamer 110 | uses: blinemedical/setup-gstreamer@v1.4.0 111 | with: 112 | version: "1.24.5" 113 | arch: "x86_64" 114 | - name: Install NPM dependencies 115 | run: npm install 116 | - name: Cache target 117 | uses: actions/cache@v3 118 | with: 119 | key: ${{ runner.OS }}-rust-cache-${{ hashFiles('**/Cargo.toml') }} 120 | path: | 121 | ~/.cargo/registry 122 | ~/.cargo/git 123 | src-tauri/target 124 | - name: Compile application 125 | run: npm run tauri build 126 | env: 127 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 128 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 129 | - uses: actions/upload-artifact@v4 130 | with: 131 | name: Release (Windows latest) 132 | path: src-tauri/target/release/bundle/ 133 | if-no-files-found: "error" 134 | 135 | release: 136 | if: startsWith(github.ref, 'refs/tags/') 137 | name: Release 138 | runs-on: ubuntu-latest 139 | needs: [build-linux, build-windows] 140 | steps: 141 | - uses: actions/checkout@v4 142 | - name: Download Linux artifacts 143 | uses: actions/download-artifact@v4 144 | with: 145 | name: Release (Ubuntu latest) 146 | path: /home/runner/artifacts/linux 147 | - name: Download Windows artifacts 148 | uses: actions/download-artifact@v4 149 | with: 150 | name: Release (Windows latest) 151 | path: /home/runner/artifacts/windows 152 | - uses: softprops/action-gh-release@v2 153 | with: 154 | repository: Recordscript/recordscript 155 | token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} 156 | files: | 157 | /home/runner/artifacts/linux/deb/*.deb 158 | /home/runner/artifacts/linux/appimage/*.AppImage 159 | /home/runner/artifacts/windows/msi/*.msi 160 | /home/runner/artifacts/windows/nsis/*.exe 161 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | node_modules 17 | 18 | dist/ 19 | 20 | vite.config.ts.timestamp* 21 | ignored/ 22 | *.webm 23 | *.mp4 24 | .env 25 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.lint.unknownAtRules": "ignore" 3 | } -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | For more details see [GitHub workflow](.github/workflows/publish.yml) 2 | 3 | # Windows 4 | ## Prerequisites 5 | - Install [Rust](https://rustup.rs/) 6 | - Install [vcpkg](https://learn.microsoft.com/en-us/vcpkg/get_started/get-started) . Make sure you've set VCPKG_ROOT and add the PATH 7 | - Install both [GStreamer 1.24.5 runtime and development](https://gstreamer.freedesktop.org/download/#windows) and add it to PATH. 8 | Make sure you've added [GStreamer bin directory and lib/gstreamer-1.0 and lib/pkgconfig to PATH](https://gstreamer.freedesktop.org/documentation/installing/on-windows.html?gi-language=c) 9 | - Install [NodeJS](https://nodejs.org/en/download/package-manager) with [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 10 | 11 | ## Building 12 | 1. Go to vcpkg directory and install `libvpx`, `libyuv`, `opus`, and `aom` with triplet `x64-windows-static` 13 | ``` 14 | ./vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static 15 | ``` 16 | 2. Go back to this repository directory and install the NPM dependencies 17 | ``` 18 | npm install 19 | ``` 20 | 3. Then build the app with Tauri 21 | ``` 22 | npm run tauri build 23 | ``` 24 | 25 | # Linux 26 | ## Prerequisites 27 | - Install [Rust](https://rustup.rs/) 28 | - Install [vcpkg](https://learn.microsoft.com/en-us/vcpkg/get_started/overview) 29 | - Install [GStreamer 1.24.5](https://gstreamer.freedesktop.org/download/#linux) both runtime and development 30 | - Install [NodeJS](https://nodejs.org/en/download/package-manager) with [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 31 | 32 | ## Building 33 | 1. Install dependencies (install equivalent package in your distro) 34 | ``` 35 | sudo apt install libasound2-dev libudev-dev nasm libxcb-randr0-dev 36 | ``` 37 | 2. Go to vcpkg directory and install `libvpx`, `libyuv`, `opus`, and `aom` 38 | ``` 39 | ./vcpkg install libvpx libyuv opus aom 40 | ``` 41 | 3. Go to the repo directory and install the NPM dependencies 42 | ``` 43 | npm install 44 | ``` 45 | 4. Then build the app with Tauri 46 | ``` 47 | npm run tauri build 48 | ``` 49 | 50 | # Mac 51 | 52 | ## Notes 53 | - For mac, you need to pull `mac` branch. The current `main` branch don't support mac build 54 | - There are still issues with the screen recorder. But, the subtitle generator (from audio/video file) works just fine 55 | 56 | ## Prerequisites 57 | - Install [Rust](https://rustup.rs/) 58 | - Install [GStreamer 1.24.5](https://gstreamer.freedesktop.org/download/#linux) both runtime and development 59 | - Install [NodeJS](https://nodejs.org/en/download/package-manager) with [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 60 | - Install vcpkg and some libraries 61 | ``` 62 | git clone https://github.com/microsoft/vcpkg 63 | cd vcpkg 64 | git checkout 2023.04.15 65 | ./bootstrap-vcpkg.sh -disableMetrics 66 | ./vcpkg install libvpx libyuv opus aom 67 | export VCPKG_ROOT=~/repos/vcpkg 68 | ``` 69 | change the /repos/vcpkg to your vcpkg repo directory 70 | 71 | ## Building 72 | 1. Go to the repo directory and install the NPM dependencies 73 | ``` 74 | npm install 75 | ``` 76 | 2. Then build the app with Tauri 77 | ``` 78 | npm run tauri build 79 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Recordscript 2 | 3 | Generate subtitle either from built-in screen recorder or from your video/audio files. 4 | 5 | Works on Windows and Ubuntu. Built with whisper-rs (Rust binding to whisper.cpp), Tauri & Rust. 6 | 7 | # Features 8 | 9 | 1. Screen Recorder with Subtitle capability 10 | 2. Subtitle Generator from a Video/Audio file with English Translation support 11 | 3. Works fully offline (thanks to Whisper.cpp!) 12 | 4. Cross-platform (thanks to Tauri!) 13 | 14 | # Download 15 | 16 | Microsoft Store: https://apps.microsoft.com/detail/9np4vrbxlm9f?hl=en-us&gl=US 17 | 18 | You can also take a look at our Release page. 19 | For now, there's no Mac build yet since there are still some issues. 20 | 21 | # Build step 22 | 23 | If you want to build the app on your local machine, take a look at `BUILDING.md` 24 | 25 | # Known Issues 26 | 27 | ## Mac 28 | 29 | On Macbook Pro M1, the screen recording's not working properly - does not save the file after stopping the recording. After some digging, it seems it was caused by the audio pipeline of GStreamer getting stuck somehow. 30 | 31 | You can help contribute on resolving this issue by pulling the branch `mac` first and submitting a PR to this repo. 32 | 33 | # Contribution 34 | 35 | We welcome everyone who would like to contribute to this project. 36 | 37 | # License 38 | 39 | We use GNU GPLv3.0 license. Take a look at `COPYING` 40 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Recordscript 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /libs/hbb_common/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /libs/hbb_common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hbb_common" 3 | version = "0.1.0" 4 | authors = ["open-trade "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | flexi_logger = { version = "0.27", features = ["async"] } 11 | protobuf = { version = "3.4", features = ["with-bytes"] } 12 | tokio = { version = "1.37", features = ["full"] } 13 | tokio-util = { version = "0.7", features = ["full"] } 14 | futures = "0.3" 15 | bytes = { version = "1.6", features = ["serde"] } 16 | log = "0.4" 17 | env_logger = "0.10" 18 | socket2 = { version = "0.3", features = ["reuseport"] } 19 | zstd = "0.13" 20 | anyhow = "1.0" 21 | futures-util = "0.3" 22 | directories-next = "2.0" 23 | rand = "0.8" 24 | serde_derive = "1.0" 25 | serde = "1.0" 26 | serde_json = "1.0" 27 | lazy_static = "1.4" 28 | confy = { git = "https://github.com/rustdesk-org/confy" } 29 | dirs-next = "2.0" 30 | filetime = "0.2" 31 | sodiumoxide = "0.2" 32 | regex = "1.8" 33 | tokio-socks = { git = "https://github.com/rustdesk-org/tokio-socks" } 34 | chrono = "0.4" 35 | backtrace = "0.3" 36 | libc = "0.2" 37 | dlopen = "0.1" 38 | toml = "0.7" 39 | uuid = { version = "1.3", features = ["v4"] } 40 | # crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052 41 | sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" } 42 | thiserror = "1.0" 43 | httparse = "1.5" 44 | base64 = "0.22" 45 | url = "2.2" 46 | 47 | [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 48 | mac_address = "1.1" 49 | machine-uid = { git = "https://github.com/21pages/machine-uid" } 50 | [target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies] 51 | tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false } 52 | rustls-platform-verifier = "0.3.1" 53 | rustls-pki-types = "1.4" 54 | [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] 55 | tokio-native-tls ="0.3" 56 | 57 | [build-dependencies] 58 | protobuf-codegen = { version = "3.4" } 59 | 60 | [target.'cfg(target_os = "windows")'.dependencies] 61 | winapi = { version = "0.3.9", features = ["winuser", "synchapi", "pdh", "memoryapi", "sysinfoapi"] } 62 | 63 | [target.'cfg(target_os = "macos")'.dependencies] 64 | osascript = "0.3" 65 | 66 | -------------------------------------------------------------------------------- /libs/hbb_common/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let out_dir = format!("{}/protos", std::env::var("OUT_DIR").unwrap()); 3 | 4 | std::fs::create_dir_all(&out_dir).unwrap(); 5 | 6 | protobuf_codegen::Codegen::new() 7 | .pure() 8 | .out_dir(out_dir) 9 | .inputs(["protos/rendezvous.proto", "protos/message.proto"]) 10 | .include("protos") 11 | .customize(protobuf_codegen::Customize::default().tokio_bytes(true)) 12 | .run() 13 | .expect("Codegen failed."); 14 | } 15 | -------------------------------------------------------------------------------- /libs/hbb_common/examples/config.rs: -------------------------------------------------------------------------------- 1 | extern crate hbb_common; 2 | 3 | fn main() { 4 | println!("{:?}", hbb_common::config::PeerConfig::load("455058072")); 5 | } 6 | -------------------------------------------------------------------------------- /libs/hbb_common/examples/system_message.rs: -------------------------------------------------------------------------------- 1 | extern crate hbb_common; 2 | #[cfg(target_os = "linux")] 3 | use hbb_common::platform::linux; 4 | #[cfg(target_os = "macos")] 5 | use hbb_common::platform::macos; 6 | 7 | fn main() { 8 | #[cfg(target_os = "linux")] 9 | let res = linux::system_message("test title", "test message", true); 10 | #[cfg(target_os = "macos")] 11 | let res = macos::alert( 12 | "System Preferences".to_owned(), 13 | "warning".to_owned(), 14 | "test title".to_owned(), 15 | "test message".to_owned(), 16 | ["Ok".to_owned()].to_vec(), 17 | ); 18 | #[cfg(any(target_os = "linux", target_os = "macos"))] 19 | println!("result {:?}", &res); 20 | } 21 | -------------------------------------------------------------------------------- /libs/hbb_common/protos/rendezvous.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package hbb; 3 | 4 | message RegisterPeer { 5 | string id = 1; 6 | int32 serial = 2; 7 | } 8 | 9 | enum ConnType { 10 | DEFAULT_CONN = 0; 11 | FILE_TRANSFER = 1; 12 | PORT_FORWARD = 2; 13 | RDP = 3; 14 | } 15 | 16 | message RegisterPeerResponse { bool request_pk = 2; } 17 | 18 | message PunchHoleRequest { 19 | string id = 1; 20 | NatType nat_type = 2; 21 | string licence_key = 3; 22 | ConnType conn_type = 4; 23 | string token = 5; 24 | } 25 | 26 | message PunchHole { 27 | bytes socket_addr = 1; 28 | string relay_server = 2; 29 | NatType nat_type = 3; 30 | string request_region = 4; 31 | } 32 | 33 | message TestNatRequest { 34 | int32 serial = 1; 35 | } 36 | 37 | // per my test, uint/int has no difference in encoding, int not good for negative, use sint for negative 38 | message TestNatResponse { 39 | int32 port = 1; 40 | ConfigUpdate cu = 2; // for mobile 41 | } 42 | 43 | enum NatType { 44 | UNKNOWN_NAT = 0; 45 | ASYMMETRIC = 1; 46 | SYMMETRIC = 2; 47 | } 48 | 49 | message PunchHoleSent { 50 | bytes socket_addr = 1; 51 | string id = 2; 52 | string relay_server = 3; 53 | NatType nat_type = 4; 54 | string version = 5; 55 | string request_region = 6; 56 | } 57 | 58 | message RegisterPk { 59 | string id = 1; 60 | bytes uuid = 2; 61 | bytes pk = 3; 62 | string old_id = 4; 63 | } 64 | 65 | message RegisterPkResponse { 66 | enum Result { 67 | OK = 0; 68 | UUID_MISMATCH = 2; 69 | ID_EXISTS = 3; 70 | TOO_FREQUENT = 4; 71 | INVALID_ID_FORMAT = 5; 72 | NOT_SUPPORT = 6; 73 | SERVER_ERROR = 7; 74 | } 75 | Result result = 1; 76 | int32 keep_alive = 2; 77 | } 78 | 79 | message PunchHoleResponse { 80 | bytes socket_addr = 1; 81 | bytes pk = 2; 82 | enum Failure { 83 | ID_NOT_EXIST = 0; 84 | OFFLINE = 2; 85 | LICENSE_MISMATCH = 3; 86 | LICENSE_OVERUSE = 4; 87 | } 88 | Failure failure = 3; 89 | string relay_server = 4; 90 | oneof union { 91 | NatType nat_type = 5; 92 | bool is_local = 6; 93 | } 94 | string other_failure = 7; 95 | } 96 | 97 | message ConfigUpdate { 98 | int32 serial = 1; 99 | repeated string rendezvous_servers = 2; 100 | } 101 | 102 | message RequestRelay { 103 | string id = 1; 104 | string uuid = 2; 105 | bytes socket_addr = 3; 106 | string relay_server = 4; 107 | bool secure = 5; 108 | string licence_key = 6; 109 | ConnType conn_type = 7; 110 | string token = 8; 111 | string request_region = 9; 112 | } 113 | 114 | message RelayResponse { 115 | bytes socket_addr = 1; 116 | string uuid = 2; 117 | string relay_server = 3; 118 | oneof union { 119 | string id = 4; 120 | bytes pk = 5; 121 | } 122 | string refuse_reason = 6; 123 | string version = 7; 124 | string request_region = 8; 125 | } 126 | 127 | message SoftwareUpdate { string url = 1; } 128 | 129 | // if in same intranet, punch hole won't work both for udp and tcp, 130 | // even some router has below connection error if we connect itself, 131 | // { kind: Other, error: "could not resolve to any address" }, 132 | // so we request local address to connect. 133 | message FetchLocalAddr { 134 | bytes socket_addr = 1; 135 | string relay_server = 2; 136 | string request_region = 3; 137 | } 138 | 139 | message LocalAddr { 140 | bytes socket_addr = 1; 141 | bytes local_addr = 2; 142 | string relay_server = 3; 143 | string id = 4; 144 | string version = 5; 145 | string request_region = 6; 146 | } 147 | 148 | message PeerDiscovery { 149 | string cmd = 1; 150 | string mac = 2; 151 | string id = 3; 152 | string username = 4; 153 | string hostname = 5; 154 | string platform = 6; 155 | string misc = 7; 156 | } 157 | 158 | message OnlineRequest { 159 | string id = 1; 160 | repeated string peers = 2; 161 | } 162 | 163 | message OnlineResponse { 164 | bytes states = 1; 165 | } 166 | 167 | message KeyExchange { 168 | repeated bytes keys = 1; 169 | } 170 | 171 | message RendezvousMessage { 172 | oneof union { 173 | RegisterPeer register_peer = 6; 174 | RegisterPeerResponse register_peer_response = 7; 175 | PunchHoleRequest punch_hole_request = 8; 176 | PunchHole punch_hole = 9; 177 | PunchHoleSent punch_hole_sent = 10; 178 | PunchHoleResponse punch_hole_response = 11; 179 | FetchLocalAddr fetch_local_addr = 12; 180 | LocalAddr local_addr = 13; 181 | ConfigUpdate configure_update = 14; 182 | RegisterPk register_pk = 15; 183 | RegisterPkResponse register_pk_response = 16; 184 | SoftwareUpdate software_update = 17; 185 | RequestRelay request_relay = 18; 186 | RelayResponse relay_response = 19; 187 | TestNatRequest test_nat_request = 20; 188 | TestNatResponse test_nat_response = 21; 189 | PeerDiscovery peer_discovery = 22; 190 | OnlineRequest online_request = 23; 191 | OnlineResponse online_response = 24; 192 | KeyExchange key_exchange = 25; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /libs/hbb_common/src/compress.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, io}; 2 | use zstd::bulk::{Compressor, Decompressor}; 3 | 4 | // The library supports regular compression levels from 1 up to ZSTD_maxCLevel(), 5 | // which is currently 22. Levels >= 20 6 | // Default level is ZSTD_CLEVEL_DEFAULT==3. 7 | // value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT 8 | thread_local! { 9 | static COMPRESSOR: RefCell>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL)); 10 | static DECOMPRESSOR: RefCell>> = RefCell::new(Decompressor::new()); 11 | } 12 | 13 | pub fn compress(data: &[u8]) -> Vec { 14 | let mut out = Vec::new(); 15 | COMPRESSOR.with(|c| { 16 | if let Ok(mut c) = c.try_borrow_mut() { 17 | match &mut *c { 18 | Ok(c) => match c.compress(data) { 19 | Ok(res) => out = res, 20 | Err(err) => { 21 | crate::log::debug!("Failed to compress: {}", err); 22 | } 23 | }, 24 | Err(err) => { 25 | crate::log::debug!("Failed to get compressor: {}", err); 26 | } 27 | } 28 | } 29 | }); 30 | out 31 | } 32 | 33 | pub fn decompress(data: &[u8]) -> Vec { 34 | let mut out = Vec::new(); 35 | DECOMPRESSOR.with(|d| { 36 | if let Ok(mut d) = d.try_borrow_mut() { 37 | match &mut *d { 38 | Ok(d) => { 39 | const MAX: usize = 1024 * 1024 * 64; 40 | const MIN: usize = 1024 * 1024; 41 | let mut n = 30 * data.len(); 42 | n = n.clamp(MIN, MAX); 43 | match d.decompress(data, n) { 44 | Ok(res) => out = res, 45 | Err(err) => { 46 | crate::log::debug!("Failed to decompress: {}", err); 47 | } 48 | } 49 | } 50 | Err(err) => { 51 | crate::log::debug!("Failed to get decompressor: {}", err); 52 | } 53 | } 54 | } 55 | }); 56 | out 57 | } 58 | -------------------------------------------------------------------------------- /libs/hbb_common/src/keyboard.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, slice::Iter, str::FromStr}; 2 | 3 | use crate::protos::message::KeyboardMode; 4 | 5 | impl fmt::Display for KeyboardMode { 6 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 7 | match self { 8 | KeyboardMode::Legacy => write!(f, "legacy"), 9 | KeyboardMode::Map => write!(f, "map"), 10 | KeyboardMode::Translate => write!(f, "translate"), 11 | KeyboardMode::Auto => write!(f, "auto"), 12 | } 13 | } 14 | } 15 | 16 | impl FromStr for KeyboardMode { 17 | type Err = (); 18 | fn from_str(s: &str) -> Result { 19 | match s { 20 | "legacy" => Ok(KeyboardMode::Legacy), 21 | "map" => Ok(KeyboardMode::Map), 22 | "translate" => Ok(KeyboardMode::Translate), 23 | "auto" => Ok(KeyboardMode::Auto), 24 | _ => Err(()), 25 | } 26 | } 27 | } 28 | 29 | impl KeyboardMode { 30 | pub fn iter() -> Iter<'static, KeyboardMode> { 31 | static KEYBOARD_MODES: [KeyboardMode; 4] = [ 32 | KeyboardMode::Legacy, 33 | KeyboardMode::Map, 34 | KeyboardMode::Translate, 35 | KeyboardMode::Auto, 36 | ]; 37 | KEYBOARD_MODES.iter() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /libs/hbb_common/src/platform/macos.rs: -------------------------------------------------------------------------------- 1 | use crate::ResultType; 2 | use osascript; 3 | use serde_derive::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize)] 6 | struct AlertParams { 7 | title: String, 8 | message: String, 9 | alert_type: String, 10 | buttons: Vec, 11 | } 12 | 13 | #[derive(Deserialize)] 14 | struct AlertResult { 15 | #[serde(rename = "buttonReturned")] 16 | button: String, 17 | } 18 | 19 | /// Firstly run the specified app, then alert a dialog. Return the clicked button value. 20 | /// 21 | /// # Arguments 22 | /// 23 | /// * `app` - The app to execute the script. 24 | /// * `alert_type` - Alert type. . informational, warning, critical 25 | /// * `title` - The alert title. 26 | /// * `message` - The alert message. 27 | /// * `buttons` - The buttons to show. 28 | pub fn alert( 29 | app: String, 30 | alert_type: String, 31 | title: String, 32 | message: String, 33 | buttons: Vec, 34 | ) -> ResultType { 35 | let script = osascript::JavaScript::new(&format!( 36 | " 37 | var App = Application('{}'); 38 | App.includeStandardAdditions = true; 39 | return App.displayAlert($params.title, {{ 40 | message: $params.message, 41 | 'as': $params.alert_type, 42 | buttons: $params.buttons, 43 | }}); 44 | ", 45 | app 46 | )); 47 | 48 | let result: AlertResult = script.execute_with_params(AlertParams { 49 | title, 50 | message, 51 | alert_type, 52 | buttons, 53 | })?; 54 | Ok(result.button) 55 | } 56 | -------------------------------------------------------------------------------- /libs/hbb_common/src/platform/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "linux")] 2 | pub mod linux; 3 | 4 | #[cfg(target_os = "macos")] 5 | pub mod macos; 6 | 7 | #[cfg(target_os = "windows")] 8 | pub mod windows; 9 | 10 | #[cfg(not(debug_assertions))] 11 | use crate::{config::Config, log}; 12 | #[cfg(not(debug_assertions))] 13 | use std::process::exit; 14 | 15 | #[cfg(not(debug_assertions))] 16 | static mut GLOBAL_CALLBACK: Option> = None; 17 | 18 | #[cfg(not(debug_assertions))] 19 | extern "C" fn breakdown_signal_handler(sig: i32) { 20 | let mut stack = vec![]; 21 | backtrace::trace(|frame| { 22 | backtrace::resolve_frame(frame, |symbol| { 23 | if let Some(name) = symbol.name() { 24 | stack.push(name.to_string()); 25 | } 26 | }); 27 | true // keep going to the next frame 28 | }); 29 | let mut info = String::default(); 30 | if stack.iter().any(|s| { 31 | s.contains(&"nouveau_pushbuf_kick") 32 | || s.to_lowercase().contains("nvidia") 33 | || s.contains("gdk_window_end_draw_frame") 34 | || s.contains("glGetString") 35 | }) { 36 | Config::set_option("allow-always-software-render".to_string(), "Y".to_string()); 37 | info = "Always use software rendering will be set.".to_string(); 38 | log::info!("{}", info); 39 | } 40 | if stack.iter().any(|s| { 41 | s.to_lowercase().contains("nvidia") 42 | || s.to_lowercase().contains("amf") 43 | || s.to_lowercase().contains("mfx") 44 | || s.contains("cuProfilerStop") 45 | }) { 46 | Config::set_option("enable-hwcodec".to_string(), "N".to_string()); 47 | info = "Perhaps hwcodec causing the crash, disable it first".to_string(); 48 | log::info!("{}", info); 49 | } 50 | log::error!( 51 | "Got signal {} and exit. stack:\n{}", 52 | sig, 53 | stack.join("\n").to_string() 54 | ); 55 | if !info.is_empty() { 56 | #[cfg(target_os = "linux")] 57 | linux::system_message( 58 | "RustDesk", 59 | &format!("Got signal {} and exit.{}", sig, info), 60 | true, 61 | ) 62 | .ok(); 63 | } 64 | unsafe { 65 | if let Some(callback) = &GLOBAL_CALLBACK { 66 | callback() 67 | } 68 | } 69 | exit(0); 70 | } 71 | 72 | #[cfg(not(debug_assertions))] 73 | pub fn register_breakdown_handler(callback: T) 74 | where 75 | T: Fn() + 'static, 76 | { 77 | unsafe { 78 | GLOBAL_CALLBACK = Some(Box::new(callback)); 79 | libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /libs/hbb_common/src/platform/windows.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::VecDeque, 3 | sync::{Arc, Mutex}, 4 | time::Instant, 5 | }; 6 | use winapi::{ 7 | shared::minwindef::{DWORD, FALSE, TRUE}, 8 | um::{ 9 | handleapi::CloseHandle, 10 | pdh::{ 11 | PdhAddEnglishCounterA, PdhCloseQuery, PdhCollectQueryData, PdhCollectQueryDataEx, 12 | PdhGetFormattedCounterValue, PdhOpenQueryA, PDH_FMT_COUNTERVALUE, PDH_FMT_DOUBLE, 13 | PDH_HCOUNTER, PDH_HQUERY, 14 | }, 15 | synchapi::{CreateEventA, WaitForSingleObject}, 16 | sysinfoapi::VerSetConditionMask, 17 | winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0}, 18 | winnt::{ 19 | HANDLE, OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION, 20 | VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR, 21 | }, 22 | }, 23 | }; 24 | 25 | lazy_static::lazy_static! { 26 | static ref CPU_USAGE_ONE_MINUTE: Arc>> = Arc::new(Mutex::new(None)); 27 | } 28 | 29 | // https://github.com/mgostIH/process_list/blob/master/src/windows/mod.rs 30 | #[repr(transparent)] 31 | pub struct RAIIHandle(pub HANDLE); 32 | 33 | impl Drop for RAIIHandle { 34 | fn drop(&mut self) { 35 | // This never gives problem except when running under a debugger. 36 | unsafe { CloseHandle(self.0) }; 37 | } 38 | } 39 | 40 | #[repr(transparent)] 41 | pub(self) struct RAIIPDHQuery(pub PDH_HQUERY); 42 | 43 | impl Drop for RAIIPDHQuery { 44 | fn drop(&mut self) { 45 | unsafe { PdhCloseQuery(self.0) }; 46 | } 47 | } 48 | 49 | pub fn start_cpu_performance_monitor() { 50 | // Code from: 51 | // https://learn.microsoft.com/en-us/windows/win32/perfctrs/collecting-performance-data 52 | // https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhcollectquerydataex 53 | // Why value lower than taskManager: 54 | // https://aaron-margosis.medium.com/task-managers-cpu-numbers-are-all-but-meaningless-2d165b421e43 55 | // Therefore we should compare with Precess Explorer rather than taskManager 56 | 57 | let f = || unsafe { 58 | // load avg or cpu usage, test with prime95. 59 | // Prefer cpu usage because we can get accurate value from Precess Explorer. 60 | // const COUNTER_PATH: &'static str = "\\System\\Processor Queue Length\0"; 61 | const COUNTER_PATH: &'static str = "\\Processor(_total)\\% Processor Time\0"; 62 | const SAMPLE_INTERVAL: DWORD = 2; // 2 second 63 | 64 | let mut ret; 65 | let mut query: PDH_HQUERY = std::mem::zeroed(); 66 | ret = PdhOpenQueryA(std::ptr::null() as _, 0, &mut query); 67 | if ret != 0 { 68 | log::error!("PdhOpenQueryA failed: 0x{:X}", ret); 69 | return; 70 | } 71 | let _query = RAIIPDHQuery(query); 72 | let mut counter: PDH_HCOUNTER = std::mem::zeroed(); 73 | ret = PdhAddEnglishCounterA(query, COUNTER_PATH.as_ptr() as _, 0, &mut counter); 74 | if ret != 0 { 75 | log::error!("PdhAddEnglishCounterA failed: 0x{:X}", ret); 76 | return; 77 | } 78 | ret = PdhCollectQueryData(query); 79 | if ret != 0 { 80 | log::error!("PdhCollectQueryData failed: 0x{:X}", ret); 81 | return; 82 | } 83 | let mut _counter_type: DWORD = 0; 84 | let mut counter_value: PDH_FMT_COUNTERVALUE = std::mem::zeroed(); 85 | let event = CreateEventA(std::ptr::null_mut(), FALSE, FALSE, std::ptr::null() as _); 86 | if event.is_null() { 87 | log::error!("CreateEventA failed"); 88 | return; 89 | } 90 | let _event: RAIIHandle = RAIIHandle(event); 91 | ret = PdhCollectQueryDataEx(query, SAMPLE_INTERVAL, event); 92 | if ret != 0 { 93 | log::error!("PdhCollectQueryDataEx failed: 0x{:X}", ret); 94 | return; 95 | } 96 | 97 | let mut queue: VecDeque = VecDeque::new(); 98 | let mut recent_valid: VecDeque = VecDeque::new(); 99 | loop { 100 | // latest one minute 101 | if queue.len() == 31 { 102 | queue.pop_front(); 103 | } 104 | if recent_valid.len() == 31 { 105 | recent_valid.pop_front(); 106 | } 107 | // allow get value within one minute 108 | if queue.len() > 0 && recent_valid.iter().filter(|v| **v).count() > queue.len() / 2 { 109 | let sum: f64 = queue.iter().map(|f| f.to_owned()).sum(); 110 | let avg = sum / (queue.len() as f64); 111 | *CPU_USAGE_ONE_MINUTE.lock().unwrap() = Some((avg, Instant::now())); 112 | } else { 113 | *CPU_USAGE_ONE_MINUTE.lock().unwrap() = None; 114 | } 115 | if WAIT_OBJECT_0 != WaitForSingleObject(event, INFINITE) { 116 | recent_valid.push_back(false); 117 | continue; 118 | } 119 | if PdhGetFormattedCounterValue( 120 | counter, 121 | PDH_FMT_DOUBLE, 122 | &mut _counter_type, 123 | &mut counter_value, 124 | ) != 0 125 | || counter_value.CStatus != 0 126 | { 127 | recent_valid.push_back(false); 128 | continue; 129 | } 130 | queue.push_back(counter_value.u.doubleValue().clone()); 131 | recent_valid.push_back(true); 132 | } 133 | }; 134 | use std::sync::Once; 135 | static ONCE: Once = Once::new(); 136 | ONCE.call_once(|| { 137 | std::thread::spawn(f); 138 | }); 139 | } 140 | 141 | pub fn cpu_uage_one_minute() -> Option { 142 | let v = CPU_USAGE_ONE_MINUTE.lock().unwrap().clone(); 143 | if let Some((v, instant)) = v { 144 | if instant.elapsed().as_secs() < 30 { 145 | return Some(v); 146 | } 147 | } 148 | None 149 | } 150 | 151 | pub fn sync_cpu_usage(cpu_usage: Option) { 152 | let v = match cpu_usage { 153 | Some(cpu_usage) => Some((cpu_usage, Instant::now())), 154 | None => None, 155 | }; 156 | *CPU_USAGE_ONE_MINUTE.lock().unwrap() = v; 157 | log::info!("cpu usage synced: {:?}", cpu_usage); 158 | } 159 | 160 | // https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 161 | // https://github.com/nodejs/node-convergence-archive/blob/e11fe0c2777561827cdb7207d46b0917ef3c42a7/deps/uv/src/win/util.c#L780 162 | pub fn is_windows_version_or_greater( 163 | os_major: u32, 164 | os_minor: u32, 165 | build_number: u32, 166 | service_pack_major: u32, 167 | service_pack_minor: u32, 168 | ) -> bool { 169 | let mut osvi: OSVERSIONINFOEXW = unsafe { std::mem::zeroed() }; 170 | osvi.dwOSVersionInfoSize = std::mem::size_of::() as DWORD; 171 | osvi.dwMajorVersion = os_major as _; 172 | osvi.dwMinorVersion = os_minor as _; 173 | osvi.dwBuildNumber = build_number as _; 174 | osvi.wServicePackMajor = service_pack_major as _; 175 | osvi.wServicePackMinor = service_pack_minor as _; 176 | 177 | let result = unsafe { 178 | let mut condition_mask = 0; 179 | let op = VER_GREATER_EQUAL; 180 | condition_mask = VerSetConditionMask(condition_mask, VER_MAJORVERSION, op); 181 | condition_mask = VerSetConditionMask(condition_mask, VER_MINORVERSION, op); 182 | condition_mask = VerSetConditionMask(condition_mask, VER_BUILDNUMBER, op); 183 | condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMAJOR, op); 184 | condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMINOR, op); 185 | 186 | VerifyVersionInfoW( 187 | &mut osvi as *mut OSVERSIONINFOEXW, 188 | VER_MAJORVERSION 189 | | VER_MINORVERSION 190 | | VER_BUILDNUMBER 191 | | VER_SERVICEPACKMAJOR 192 | | VER_SERVICEPACKMINOR, 193 | condition_mask, 194 | ) 195 | }; 196 | 197 | result == TRUE 198 | } 199 | -------------------------------------------------------------------------------- /libs/hbb_common/src/protos/mod.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); 2 | -------------------------------------------------------------------------------- /libs/hbb_common/src/udp.rs: -------------------------------------------------------------------------------- 1 | use crate::ResultType; 2 | use anyhow::{anyhow, Context}; 3 | use bytes::{Bytes, BytesMut}; 4 | use futures::{SinkExt, StreamExt}; 5 | use protobuf::Message; 6 | use socket2::{Domain, Socket, Type}; 7 | use std::net::SocketAddr; 8 | use tokio::net::{lookup_host, ToSocketAddrs, UdpSocket}; 9 | use tokio_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs}; 10 | use tokio_util::{codec::BytesCodec, udp::UdpFramed}; 11 | 12 | pub enum FramedSocket { 13 | Direct(UdpFramed), 14 | ProxySocks(Socks5UdpFramed), 15 | } 16 | 17 | fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result { 18 | let socket = match addr { 19 | SocketAddr::V4(..) => Socket::new(Domain::ipv4(), Type::dgram(), None), 20 | SocketAddr::V6(..) => Socket::new(Domain::ipv6(), Type::dgram(), None), 21 | }?; 22 | if reuse { 23 | // windows has no reuse_port, but it's reuse_address 24 | // almost equals to unix's reuse_port + reuse_address, 25 | // though may introduce nondeterministic behavior 26 | #[cfg(unix)] 27 | socket.set_reuse_port(true).ok(); 28 | socket.set_reuse_address(true).ok(); 29 | } 30 | // only nonblocking work with tokio, https://stackoverflow.com/questions/64649405/receiver-on-tokiompscchannel-only-receives-messages-when-buffer-is-full 31 | socket.set_nonblocking(true)?; 32 | if buf_size > 0 { 33 | socket.set_recv_buffer_size(buf_size).ok(); 34 | } 35 | log::debug!( 36 | "Receive buf size of udp {}: {:?}", 37 | addr, 38 | socket.recv_buffer_size() 39 | ); 40 | if addr.is_ipv6() && addr.ip().is_unspecified() && addr.port() > 0 { 41 | socket.set_only_v6(false).ok(); 42 | } 43 | socket.bind(&addr.into())?; 44 | Ok(socket) 45 | } 46 | 47 | impl FramedSocket { 48 | pub async fn new(addr: T) -> ResultType { 49 | Self::new_reuse(addr, false, 0).await 50 | } 51 | 52 | pub async fn new_reuse( 53 | addr: T, 54 | reuse: bool, 55 | buf_size: usize, 56 | ) -> ResultType { 57 | let addr = lookup_host(&addr) 58 | .await? 59 | .next() 60 | .context("could not resolve to any address")?; 61 | Ok(Self::Direct(UdpFramed::new( 62 | UdpSocket::from_std(new_socket(addr, reuse, buf_size)?.into_udp_socket())?, 63 | BytesCodec::new(), 64 | ))) 65 | } 66 | 67 | pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>( 68 | proxy: P, 69 | local: T, 70 | username: &'a str, 71 | password: &'a str, 72 | ms_timeout: u64, 73 | ) -> ResultType { 74 | let framed = if username.trim().is_empty() { 75 | super::timeout(ms_timeout, Socks5UdpFramed::connect(proxy, Some(local))).await?? 76 | } else { 77 | super::timeout( 78 | ms_timeout, 79 | Socks5UdpFramed::connect_with_password(proxy, Some(local), username, password), 80 | ) 81 | .await?? 82 | }; 83 | log::trace!( 84 | "Socks5 udp connected, local addr: {:?}, target addr: {}", 85 | framed.local_addr(), 86 | framed.socks_addr() 87 | ); 88 | Ok(Self::ProxySocks(framed)) 89 | } 90 | 91 | #[inline] 92 | pub async fn send( 93 | &mut self, 94 | msg: &impl Message, 95 | addr: impl IntoTargetAddr<'_>, 96 | ) -> ResultType<()> { 97 | let addr = addr.into_target_addr()?.to_owned(); 98 | let send_data = Bytes::from(msg.write_to_bytes()?); 99 | match self { 100 | Self::Direct(f) => { 101 | if let TargetAddr::Ip(addr) = addr { 102 | f.send((send_data, addr)).await? 103 | } 104 | } 105 | Self::ProxySocks(f) => f.send((send_data, addr)).await?, 106 | }; 107 | Ok(()) 108 | } 109 | 110 | // https://stackoverflow.com/a/68733302/1926020 111 | #[inline] 112 | pub async fn send_raw( 113 | &mut self, 114 | msg: &'static [u8], 115 | addr: impl IntoTargetAddr<'static>, 116 | ) -> ResultType<()> { 117 | let addr = addr.into_target_addr()?.to_owned(); 118 | 119 | match self { 120 | Self::Direct(f) => { 121 | if let TargetAddr::Ip(addr) = addr { 122 | f.send((Bytes::from(msg), addr)).await? 123 | } 124 | } 125 | Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?, 126 | }; 127 | Ok(()) 128 | } 129 | 130 | #[inline] 131 | pub async fn next(&mut self) -> Option)>> { 132 | match self { 133 | Self::Direct(f) => match f.next().await { 134 | Some(Ok((data, addr))) => { 135 | Some(Ok((data, addr.into_target_addr().ok()?.to_owned()))) 136 | } 137 | Some(Err(e)) => Some(Err(anyhow!(e))), 138 | None => None, 139 | }, 140 | Self::ProxySocks(f) => match f.next().await { 141 | Some(Ok((data, _))) => Some(Ok((data.data, data.dst_addr))), 142 | Some(Err(e)) => Some(Err(anyhow!(e))), 143 | None => None, 144 | }, 145 | } 146 | } 147 | 148 | #[inline] 149 | pub async fn next_timeout( 150 | &mut self, 151 | ms: u64, 152 | ) -> Option)>> { 153 | if let Ok(res) = 154 | tokio::time::timeout(std::time::Duration::from_millis(ms), self.next()).await 155 | { 156 | res 157 | } else { 158 | None 159 | } 160 | } 161 | 162 | pub fn local_addr(&self) -> Option { 163 | if let FramedSocket::Direct(x) = self { 164 | if let Ok(v) = x.get_ref().local_addr() { 165 | return Some(v); 166 | } 167 | } 168 | None 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /libs/prebuilt/windows/avcodec-60.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/avcodec-60.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/avfilter-9.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/avfilter-9.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/avformat-60.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/avformat-60.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/avutil-58.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/avutil-58.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/bz2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/bz2.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/ffi-7.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/ffi-7.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gio-2.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gio-2.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/glib-2.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/glib-2.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gmodule-2.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gmodule-2.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gobject-2.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gobject-2.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstapp-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstapp-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstapp.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstapp.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstaudio-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstaudio-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstaudioconvert.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstaudioconvert.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstaudiomixer.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstaudiomixer.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstaudioparsers.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstaudioparsers.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstaudioresample.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstaudioresample.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstbase-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstbase-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstcodecparsers-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstcodecparsers-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstcodecs-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstcodecs-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstcoreelements.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstcoreelements.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstd3d11-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstd3d11-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstd3d11.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstd3d11.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstdxva-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstdxva-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstid3demux.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstid3demux.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstisomp4.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstisomp4.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstlibav.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstlibav.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstmatroska.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstmatroska.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstogg.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstogg.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstpbutils-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstpbutils-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstplayback.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstplayback.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstpng.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstpng.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstrawparse.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstrawparse.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstreamer-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstreamer-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstriff-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstriff-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstrtp-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstrtp-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gsttag-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gsttag-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gsttypefindfunctions.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gsttypefindfunctions.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstvideo-1.0-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstvideo-1.0-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstvideoconvertscale.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstvideoconvertscale.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstvideoparsersbad.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstvideoparsersbad.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstvorbis.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstvorbis.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstwavparse.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstwavparse.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/gstx264.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/gstx264.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/intl-8.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/intl-8.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/libFLAC-8.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/libFLAC-8.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/libpng16-16.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/libpng16-16.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/libvorbis-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/libvorbis-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/libvorbisenc-2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/libvorbisenc-2.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/libvorbisfile-3.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/libvorbisfile-3.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/libx264-157.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/libx264-157.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/msvcp140.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/msvcp140.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/ogg-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/ogg-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/orc-0.4-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/orc-0.4-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/pcre2-8-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/pcre2-8-0.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/swresample-4.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/swresample-4.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/vcruntime140.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/vcruntime140.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/vcruntime140_1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/vcruntime140_1.dll -------------------------------------------------------------------------------- /libs/prebuilt/windows/z-1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/prebuilt/windows/z-1.dll -------------------------------------------------------------------------------- /libs/scrap/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | generated/ 5 | -------------------------------------------------------------------------------- /libs/scrap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scrap" 3 | description = "Screen capture made easy." 4 | version = "0.5.0" 5 | repository = "https://github.com/quadrupleslap/scrap" 6 | documentation = "https://docs.rs/scrap" 7 | keywords = ["screen", "capture", "record"] 8 | license = "MIT" 9 | authors = ["Ram "] 10 | edition = "2018" 11 | 12 | [features] 13 | wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing"] 14 | mediacodec = ["ndk"] 15 | linux-pkg-config = ["dep:pkg-config"] 16 | hwcodec = ["dep:hwcodec"] 17 | vram = ["hwcodec/vram"] 18 | 19 | [dependencies] 20 | cfg-if = "1.0" 21 | num_cpus = "1.15" 22 | lazy_static = "1.4" 23 | hbb_common = { path = "../hbb_common" } 24 | webm = { git = "https://github.com/21pages/rust-webm" } 25 | serde = {version="1.0", features=["derive"]} 26 | 27 | [dependencies.winapi] 28 | version = "0.3.9" 29 | default-features = true 30 | features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11", "winuser", "winerror", "errhandlingapi", "libloaderapi"] 31 | 32 | [target.'cfg(target_os = "macos")'.dependencies] 33 | block = "0.1" 34 | 35 | [target.'cfg(target_os = "android")'.dependencies] 36 | android_logger = "0.13" 37 | jni = "0.21" 38 | lazy_static = "1.4" 39 | log = "0.4" 40 | serde_json = "1.0" 41 | ndk = { version = "0.7", features = ["media"], optional = true} 42 | ndk-context = "0.1" 43 | 44 | [target.'cfg(not(target_os = "android"))'.dev-dependencies] 45 | repng = "0.2" 46 | docopt = "1.1" 47 | quest = "0.3" 48 | 49 | [build-dependencies] 50 | target_build_utils = "0.3" 51 | bindgen = "0.65" 52 | pkg-config = { version = "0.3.27", optional = true } 53 | 54 | [target.'cfg(target_os = "linux")'.dependencies] 55 | dbus = { version = "0.9", optional = true } 56 | tracing = { version = "0.1", optional = true } 57 | gstreamer = { version = "0.16", optional = true } 58 | gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } 59 | gstreamer-video = { version = "0.16", optional = true } 60 | 61 | [dependencies.hwcodec] 62 | git = "https://github.com/21pages/hwcodec" 63 | optional = true 64 | 65 | 66 | -------------------------------------------------------------------------------- /libs/scrap/README.md: -------------------------------------------------------------------------------- 1 | Derived from https://github.com/quadrupleslap/scrap 2 | 3 | # scrap 4 | 5 | Scrap records your screen! At least it does if you're on Windows, macOS, or Linux. 6 | 7 | ## Usage 8 | 9 | ```toml 10 | [dependencies] 11 | scrap = "0.5" 12 | ``` 13 | 14 | Its API is as simple as it gets! 15 | 16 | ```rust 17 | struct Display; /// A screen. 18 | struct Frame; /// An array of the pixels that were on-screen. 19 | struct Capturer; /// A recording instance. 20 | 21 | impl Capturer { 22 | /// Begin recording. 23 | pub fn new(display: Display) -> io::Result; 24 | 25 | /// Try to get a frame. 26 | /// Returns WouldBlock if it's not ready yet. 27 | pub fn frame<'a>(&'a mut self) -> io::Result>; 28 | 29 | pub fn width(&self) -> usize; 30 | pub fn height(&self) -> usize; 31 | } 32 | 33 | impl Display { 34 | /// The primary screen. 35 | pub fn primary() -> io::Result; 36 | 37 | /// All the screens. 38 | pub fn all() -> io::Result>; 39 | 40 | pub fn width(&self) -> usize; 41 | pub fn height(&self) -> usize; 42 | } 43 | 44 | impl<'a> ops::Deref for Frame<'a> { 45 | /// A frame is just an array of bytes. 46 | type Target = [u8]; 47 | } 48 | ``` 49 | 50 | ## The Frame Format 51 | 52 | - The frame format is guaranteed to be **packed BGRA**. 53 | - The width and height are guaranteed to remain constant. 54 | - The stride might be greater than the width, and it may also vary between frames. 55 | 56 | ## System Requirements 57 | 58 | OS | Minimum Requirements 59 | --------|--------------------- 60 | macOS | macOS 10.8 61 | Linux | XCB + SHM + RandR 62 | Windows | DirectX 11.1 63 | -------------------------------------------------------------------------------- /libs/scrap/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, fs, 3 | path::{Path, PathBuf}, 4 | println, 5 | }; 6 | 7 | #[cfg(all(target_os = "linux", feature = "linux-pkg-config"))] 8 | fn link_pkg_config(name: &str) -> Vec { 9 | // sometimes an override is needed 10 | let pc_name = match name { 11 | "libvpx" => "vpx", 12 | _ => name, 13 | }; 14 | let lib = pkg_config::probe_library(pc_name) 15 | .expect(format!( 16 | "unable to find '{pc_name}' development headers with pkg-config (feature linux-pkg-config is enabled). 17 | try installing '{pc_name}-dev' from your system package manager.").as_str()); 18 | 19 | lib.include_paths 20 | } 21 | #[cfg(not(all(target_os = "linux", feature = "linux-pkg-config")))] 22 | fn link_pkg_config(_name: &str) -> Vec { 23 | unimplemented!() 24 | } 25 | 26 | /// Link vcpkg package. 27 | fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf { 28 | let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); 29 | let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); 30 | if target_arch == "x86_64" { 31 | target_arch = "x64".to_owned(); 32 | } else if target_arch == "x86" { 33 | target_arch = "x86".to_owned(); 34 | } else if target_arch == "loongarch64" { 35 | target_arch = "loongarch64".to_owned(); 36 | } else if target_arch == "aarch64" { 37 | target_arch = "arm64".to_owned(); 38 | } else { 39 | target_arch = "arm".to_owned(); 40 | } 41 | let mut target = if target_os == "macos" { 42 | if target_arch == "x64" { 43 | "x64-osx".to_owned() 44 | } else if target_arch == "arm64" { 45 | "arm64-osx".to_owned() 46 | } else { 47 | format!("{}-{}", target_arch, target_os) 48 | } 49 | } else if target_os == "windows" { 50 | "x64-windows-static".to_owned() 51 | } else { 52 | format!("{}-{}", target_arch, target_os) 53 | }; 54 | if target_arch == "x86" { 55 | target = target.replace("x64", "x86"); 56 | } 57 | println!("cargo:info={}", target); 58 | path.push("installed"); 59 | path.push(target); 60 | println!( 61 | "{}", 62 | format!( 63 | "cargo:rustc-link-lib=static={}", 64 | name.trim_start_matches("lib") 65 | ) 66 | ); 67 | println!( 68 | "{}", 69 | format!( 70 | "cargo:rustc-link-search={}", 71 | path.join("lib").to_str().unwrap() 72 | ) 73 | ); 74 | let include = path.join("include"); 75 | println!("{}", format!("cargo:include={}", include.to_str().unwrap())); 76 | include 77 | } 78 | 79 | /// Link homebrew package(for Mac M1). 80 | fn link_homebrew_m1(name: &str) -> PathBuf { 81 | let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); 82 | let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); 83 | if target_os != "macos" || target_arch != "aarch64" { 84 | panic!("Couldn't find VCPKG_ROOT, also can't fallback to homebrew because it's only for macos aarch64."); 85 | } 86 | let mut path = PathBuf::from("/opt/homebrew/Cellar"); 87 | path.push(name); 88 | let entries = if let Ok(dir) = std::fs::read_dir(&path) { 89 | dir 90 | } else { 91 | panic!("Could not find package in {}. Make sure your homebrew and package {} are all installed.", path.to_str().unwrap(),&name); 92 | }; 93 | let mut directories = entries 94 | .into_iter() 95 | .filter(|x| x.is_ok()) 96 | .map(|x| x.unwrap().path()) 97 | .filter(|x| x.is_dir()) 98 | .collect::>(); 99 | // Find the newest version. 100 | directories.sort_unstable(); 101 | if directories.is_empty() { 102 | panic!( 103 | "There's no installed version of {} in /opt/homebrew/Cellar", 104 | name 105 | ); 106 | } 107 | path.push(directories.pop().unwrap()); 108 | // Link the library. 109 | println!( 110 | "{}", 111 | format!( 112 | "cargo:rustc-link-lib=static={}", 113 | name.trim_start_matches("lib") 114 | ) 115 | ); 116 | // Add the library path. 117 | println!( 118 | "{}", 119 | format!( 120 | "cargo:rustc-link-search={}", 121 | path.join("lib").to_str().unwrap() 122 | ) 123 | ); 124 | // Add the include path. 125 | let include = path.join("include"); 126 | println!("{}", format!("cargo:include={}", include.to_str().unwrap())); 127 | include 128 | } 129 | 130 | /// Find package. By default, it will try to find vcpkg first, then homebrew(currently only for Mac M1). 131 | /// If building for linux and feature "linux-pkg-config" is enabled, will try to use pkg-config 132 | /// unless check fails (e.g. NO_PKG_CONFIG_libyuv=1) 133 | fn find_package(name: &str) -> Vec { 134 | let no_pkg_config_var_name = format!("NO_PKG_CONFIG_{name}"); 135 | println!("cargo:rerun-if-env-changed={no_pkg_config_var_name}"); 136 | if cfg!(all(target_os = "linux", feature = "linux-pkg-config")) 137 | && std::env::var(no_pkg_config_var_name).as_deref() != Ok("1") 138 | { 139 | link_pkg_config(name) 140 | } else if let Ok(vcpkg_root) = std::env::var("VCPKG_ROOT") { 141 | vec![link_vcpkg(vcpkg_root.into(), name)] 142 | } else { 143 | // Try using homebrew 144 | vec![link_homebrew_m1(name)] 145 | } 146 | } 147 | 148 | fn generate_bindings( 149 | ffi_header: &Path, 150 | include_paths: &[PathBuf], 151 | ffi_rs: &Path, 152 | exact_file: &Path, 153 | regex: &str, 154 | ) { 155 | let mut b = bindgen::builder() 156 | .header(ffi_header.to_str().unwrap()) 157 | .allowlist_type(regex) 158 | .allowlist_var(regex) 159 | .allowlist_function(regex) 160 | .rustified_enum(regex) 161 | .trust_clang_mangling(false) 162 | .layout_tests(false) // breaks 32/64-bit compat 163 | .generate_comments(false); // comments have prefix /*!\ 164 | 165 | for dir in include_paths { 166 | b = b.clang_arg(format!("-I{}", dir.display())); 167 | } 168 | 169 | b.generate().unwrap().write_to_file(ffi_rs).unwrap(); 170 | fs::copy(ffi_rs, exact_file).ok(); // ignore failure 171 | } 172 | 173 | fn gen_vcpkg_package(package: &str, ffi_header: &str, generated: &str, regex: &str) { 174 | let includes = find_package(package); 175 | let src_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap(); 176 | let src_dir = Path::new(&src_dir); 177 | let out_dir = env::var_os("OUT_DIR").unwrap(); 178 | let out_dir = Path::new(&out_dir); 179 | 180 | let ffi_header = src_dir.join("src").join("bindings").join(ffi_header); 181 | println!("rerun-if-changed={}", ffi_header.display()); 182 | for dir in &includes { 183 | println!("rerun-if-changed={}", dir.display()); 184 | } 185 | 186 | let ffi_rs = out_dir.join(generated); 187 | let exact_file = src_dir.join("generated").join(generated); 188 | generate_bindings(&ffi_header, &includes, &ffi_rs, &exact_file, regex); 189 | } 190 | 191 | fn main() { 192 | // note: all link symbol names in x86 (32-bit) are prefixed wth "_". 193 | // run "rustup show" to show current default toolchain, if it is stable-x86-pc-windows-msvc, 194 | // please install x64 toolchain by "rustup toolchain install stable-x86_64-pc-windows-msvc", 195 | // then set x64 to default by "rustup default stable-x86_64-pc-windows-msvc" 196 | let target = target_build_utils::TargetInfo::new(); 197 | if target.unwrap().target_pointer_width() != "64" { 198 | // panic!("Only support 64bit system"); 199 | } 200 | env::remove_var("CARGO_CFG_TARGET_FEATURE"); 201 | env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static"); 202 | 203 | find_package("libyuv"); 204 | gen_vcpkg_package("libvpx", "vpx_ffi.h", "vpx_ffi.rs", "^[vV].*"); 205 | gen_vcpkg_package("aom", "aom_ffi.h", "aom_ffi.rs", "^(aom|AOM|OBU|AV1).*"); 206 | gen_vcpkg_package("libyuv", "yuv_ffi.h", "yuv_ffi.rs", ".*"); 207 | 208 | // there is problem with cfg(target_os) in build.rs, so use our workaround 209 | let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); 210 | if target_os == "ios" { 211 | // nothing 212 | } else if target_os == "android" { 213 | println!("cargo:rustc-cfg=android"); 214 | } else if cfg!(windows) { 215 | // The first choice is Windows because DXGI is amazing. 216 | println!("cargo:rustc-cfg=dxgi"); 217 | } else if cfg!(target_os = "macos") { 218 | // Quartz is second because macOS is the (annoying) exception. 219 | println!("cargo:rustc-cfg=quartz"); 220 | } else if cfg!(unix) { 221 | // On UNIX we pray that X11 (with XCB) is available. 222 | println!("cargo:rustc-cfg=x11"); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /libs/scrap/examples/capture_mag.rs: -------------------------------------------------------------------------------- 1 | extern crate repng; 2 | extern crate scrap; 3 | 4 | use scrap::Display; 5 | #[cfg(windows)] 6 | use scrap::{CapturerMag, TraitCapturer}; 7 | #[cfg(windows)] 8 | use std::fs::File; 9 | 10 | fn main() { 11 | let n = Display::all().unwrap().len(); 12 | for _i in 0..n { 13 | #[cfg(windows)] 14 | record(_i); 15 | } 16 | } 17 | 18 | #[cfg(windows)] 19 | fn get_display(i: usize) -> Display { 20 | Display::all().unwrap().remove(i) 21 | } 22 | 23 | #[cfg(windows)] 24 | fn record(i: usize) { 25 | use std::time::Duration; 26 | 27 | use scrap::{Frame, TraitPixelBuffer}; 28 | 29 | for d in Display::all().unwrap() { 30 | println!("{:?} {} {}", d.origin(), d.width(), d.height()); 31 | } 32 | 33 | let display = get_display(i); 34 | let (w, h) = (display.width(), display.height()); 35 | 36 | { 37 | let mut capture_mag = CapturerMag::new(display.origin(), display.width(), display.height()) 38 | .expect("Couldn't begin capture."); 39 | let wnd_cls = ""; 40 | let wnd_name = "RustDeskPrivacyWindow"; 41 | if false == capture_mag.exclude(wnd_cls, wnd_name).unwrap() { 42 | println!("No window found for cls {} name {}", wnd_cls, wnd_name); 43 | } else { 44 | println!("Filter window for cls {} name {}", wnd_cls, wnd_name); 45 | } 46 | 47 | let frame = capture_mag.frame(Duration::from_millis(0)).unwrap(); 48 | let Frame::PixelBuffer(frame) = frame else { 49 | return; 50 | }; 51 | let frame = frame.data(); 52 | println!("Capture data len: {}, Saving...", frame.len()); 53 | 54 | let mut bitflipped = Vec::with_capacity(w * h * 4); 55 | let stride = frame.len() / h; 56 | 57 | for y in 0..h { 58 | for x in 0..w { 59 | let i = stride * y + 4 * x; 60 | bitflipped.extend_from_slice(&[frame[i + 2], frame[i + 1], frame[i], 255]); 61 | } 62 | } 63 | // Save the image. 64 | let name = format!("capture_mag_{}_1.png", i); 65 | repng::encode( 66 | File::create(name.clone()).unwrap(), 67 | w as u32, 68 | h as u32, 69 | &bitflipped, 70 | ) 71 | .unwrap(); 72 | println!("Image saved to `{}`.", name); 73 | } 74 | 75 | { 76 | let mut capture_mag = CapturerMag::new(display.origin(), display.width(), display.height()) 77 | .expect("Couldn't begin capture."); 78 | let wnd_cls = ""; 79 | let wnd_title = "RustDeskPrivacyWindow"; 80 | if false == capture_mag.exclude(wnd_cls, wnd_title).unwrap() { 81 | println!("No window found for cls {} title {}", wnd_cls, wnd_title); 82 | } else { 83 | println!("Filter window for cls {} title {}", wnd_cls, wnd_title); 84 | } 85 | 86 | let frame = capture_mag.frame(Duration::from_millis(0)).unwrap(); 87 | let Frame::PixelBuffer(frame) = frame else { 88 | return; 89 | }; 90 | println!("Capture data len: {}, Saving...", frame.data().len()); 91 | 92 | let mut raw = Vec::new(); 93 | unsafe { 94 | scrap::ARGBToRAW( 95 | frame.data().as_ptr(), 96 | frame.stride()[0] as _, 97 | (&mut raw).as_mut_ptr(), 98 | (w * 3) as _, 99 | w as _, 100 | h as _, 101 | ) 102 | }; 103 | 104 | let mut bitflipped = Vec::with_capacity(w * h * 4); 105 | let stride = raw.len() / h; 106 | 107 | for y in 0..h { 108 | for x in 0..w { 109 | let i = stride * y + 3 * x; 110 | bitflipped.extend_from_slice(&[raw[i], raw[i + 1], raw[i + 2], 255]); 111 | } 112 | } 113 | let name = format!("capture_mag_{}_2.png", i); 114 | repng::encode( 115 | File::create(name.clone()).unwrap(), 116 | w as u32, 117 | h as u32, 118 | &bitflipped, 119 | ) 120 | .unwrap(); 121 | 122 | println!("Image saved to `{}`.", name); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /libs/scrap/examples/ffplay.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use scrap::{Frame, TraitPixelBuffer}; 4 | 5 | extern crate scrap; 6 | 7 | fn main() { 8 | use scrap::{Capturer, Display, TraitCapturer}; 9 | use std::io::ErrorKind::WouldBlock; 10 | use std::io::Write; 11 | use std::process::{Command, Stdio}; 12 | 13 | let d = Display::primary().unwrap(); 14 | let (w, h) = (d.width(), d.height()); 15 | 16 | let child = Command::new("ffplay") 17 | .args(&[ 18 | "-f", 19 | "rawvideo", 20 | "-pixel_format", 21 | "bgr0", 22 | "-video_size", 23 | &format!("{}x{}", w, h), 24 | "-framerate", 25 | "60", 26 | "-", 27 | ]) 28 | .stdin(Stdio::piped()) 29 | .spawn() 30 | .expect("This example requires ffplay."); 31 | 32 | let mut capturer = Capturer::new(d).unwrap(); 33 | let mut out = child.stdin.unwrap(); 34 | 35 | loop { 36 | match capturer.frame(Duration::from_millis(0)) { 37 | Ok(frame) => { 38 | // Write the frame, removing end-of-row padding. 39 | let Frame::PixelBuffer(frame) = frame else { 40 | return; 41 | }; 42 | let stride = frame.stride()[0]; 43 | let rowlen = 4 * w; 44 | for row in frame.data().chunks(stride) { 45 | let row = &row[..rowlen]; 46 | out.write_all(row).unwrap(); 47 | } 48 | } 49 | Err(ref e) if e.kind() == WouldBlock => { 50 | // Wait for the frame. 51 | } 52 | Err(_) => { 53 | // We're done here. 54 | break; 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /libs/scrap/examples/list.rs: -------------------------------------------------------------------------------- 1 | extern crate scrap; 2 | 3 | use scrap::Display; 4 | 5 | fn main() { 6 | let displays = Display::all().unwrap(); 7 | 8 | for (i, display) in displays.iter().enumerate() { 9 | println!( 10 | "Display {} [{}x{}]", 11 | i + 1, 12 | display.width(), 13 | display.height() 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libs/scrap/examples/record-screen.rs: -------------------------------------------------------------------------------- 1 | extern crate docopt; 2 | extern crate quest; 3 | extern crate repng; 4 | extern crate scrap; 5 | extern crate serde; 6 | extern crate webm; 7 | 8 | use std::fs::{File, OpenOptions}; 9 | use std::path::PathBuf; 10 | use std::sync::atomic::{AtomicBool, Ordering}; 11 | use std::sync::Arc; 12 | use std::time::{Duration, Instant}; 13 | use std::{io, thread}; 14 | 15 | use docopt::Docopt; 16 | use scrap::codec::{EncoderApi, EncoderCfg, Quality as Q}; 17 | use webm::mux; 18 | use webm::mux::Track; 19 | 20 | use scrap::vpxcodec as vpx_encode; 21 | use scrap::{Capturer, Display, TraitCapturer, STRIDE_ALIGN}; 22 | 23 | const USAGE: &'static str = " 24 | Simple WebM screen capture. 25 | 26 | Usage: 27 | record-screen [--time=] [--fps=] [--quality=] [--ba=] [--codec CODEC] 28 | record-screen (-h | --help) 29 | 30 | Options: 31 | -h --help Show this screen. 32 | --time= Recording duration in seconds. 33 | --fps= Frames per second [default: 30]. 34 | --quality= Video quality [default: Balanced]. 35 | Valid values: Best, Balanced, Low. 36 | --ba= Audio bitrate in kilobits per second [default: 96]. 37 | --codec CODEC Configure the codec used. [default: vp9] 38 | Valid values: vp8, vp9. 39 | "; 40 | 41 | #[derive(Debug, serde::Deserialize)] 42 | struct Args { 43 | arg_path: PathBuf, 44 | flag_codec: Codec, 45 | flag_time: Option, 46 | flag_fps: u64, 47 | flag_quality: Quality, 48 | } 49 | 50 | #[derive(Debug, serde::Deserialize)] 51 | enum Quality { 52 | Best, 53 | Balanced, 54 | Low, 55 | } 56 | 57 | #[derive(Debug, serde::Deserialize)] 58 | enum Codec { 59 | Vp8, 60 | Vp9, 61 | } 62 | 63 | fn main() -> io::Result<()> { 64 | let args: Args = Docopt::new(USAGE) 65 | .and_then(|d| d.deserialize()) 66 | .unwrap_or_else(|e| e.exit()); 67 | 68 | let duration = args.flag_time.map(Duration::from_secs); 69 | 70 | let d = Display::primary().unwrap(); 71 | let (width, height) = (d.width() as u32, d.height() as u32); 72 | 73 | // Setup the multiplexer. 74 | 75 | let out = match { 76 | OpenOptions::new() 77 | .write(true) 78 | .create_new(true) 79 | .open(&args.arg_path) 80 | } { 81 | Ok(file) => file, 82 | Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => { 83 | if loop { 84 | quest::ask("Overwrite the existing file? [y/N] "); 85 | if let Some(b) = quest::yesno(false)? { 86 | break b; 87 | } 88 | } { 89 | File::create(&args.arg_path)? 90 | } else { 91 | return Ok(()); 92 | } 93 | } 94 | Err(e) => return Err(e.into()), 95 | }; 96 | 97 | let mut webm = 98 | mux::Segment::new(mux::Writer::new(out)).expect("Could not initialize the multiplexer."); 99 | 100 | let (vpx_codec, mux_codec) = match args.flag_codec { 101 | Codec::Vp8 => (vpx_encode::VpxVideoCodecId::VP8, mux::VideoCodecId::VP8), 102 | Codec::Vp9 => (vpx_encode::VpxVideoCodecId::VP9, mux::VideoCodecId::VP9), 103 | }; 104 | 105 | let mut vt = webm.add_video_track(width, height, None, mux_codec); 106 | 107 | // Setup the encoder. 108 | let quality = match args.flag_quality { 109 | Quality::Best => Q::Best, 110 | Quality::Balanced => Q::Balanced, 111 | Quality::Low => Q::Low, 112 | }; 113 | let mut vpx = vpx_encode::VpxEncoder::new( 114 | EncoderCfg::VPX(vpx_encode::VpxEncoderConfig { 115 | width, 116 | height, 117 | quality, 118 | codec: vpx_codec, 119 | keyframe_interval: None, 120 | }), 121 | false, 122 | ) 123 | .unwrap(); 124 | 125 | // Start recording. 126 | 127 | let start = Instant::now(); 128 | let stop = Arc::new(AtomicBool::new(false)); 129 | 130 | thread::spawn({ 131 | let stop = stop.clone(); 132 | move || { 133 | let _ = quest::ask("Recording! Press ⏎ to stop."); 134 | let _ = quest::text(); 135 | stop.store(true, Ordering::Release); 136 | } 137 | }); 138 | 139 | let spf = Duration::from_nanos(1_000_000_000 / args.flag_fps); 140 | 141 | // Capturer object is expensive, avoiding to create it frequently. 142 | let mut c = Capturer::new(d).unwrap(); 143 | let mut yuv = Vec::new(); 144 | let mut mid_data = Vec::new(); 145 | while !stop.load(Ordering::Acquire) { 146 | let now = Instant::now(); 147 | let time = now - start; 148 | 149 | if Some(true) == duration.map(|d| time > d) { 150 | break; 151 | } 152 | 153 | if let Ok(frame) = c.frame(Duration::from_millis(0)) { 154 | let ms = time.as_secs() * 1000 + time.subsec_millis() as u64; 155 | frame.to(vpx.yuvfmt(), &mut yuv, &mut mid_data).unwrap(); 156 | for frame in vpx.encode(ms as i64, &yuv, STRIDE_ALIGN).unwrap() { 157 | vt.add_frame(frame.data, frame.pts as u64 * 1_000_000, frame.key); 158 | } 159 | } 160 | 161 | let dt = now.elapsed(); 162 | if dt < spf { 163 | thread::sleep(spf - dt); 164 | } 165 | } 166 | 167 | // End things. 168 | 169 | let _ = webm.finalize(None); 170 | 171 | Ok(()) 172 | } 173 | -------------------------------------------------------------------------------- /libs/scrap/examples/screenshot.rs: -------------------------------------------------------------------------------- 1 | extern crate repng; 2 | extern crate scrap; 3 | 4 | use std::fs::File; 5 | use std::io::ErrorKind::WouldBlock; 6 | use std::thread; 7 | use std::time::Duration; 8 | 9 | use scrap::{Capturer, Display, Frame, TraitCapturer, TraitPixelBuffer}; 10 | 11 | fn main() { 12 | let n = Display::all().unwrap().len(); 13 | for i in 0..n { 14 | record(i); 15 | } 16 | } 17 | 18 | fn get_display(i: usize) -> Display { 19 | Display::all().unwrap().remove(i) 20 | } 21 | 22 | fn record(i: usize) { 23 | let one_second = Duration::new(1, 0); 24 | let one_frame = one_second / 60; 25 | 26 | for d in Display::all().unwrap() { 27 | println!("{:?} {} {}", d.origin(), d.width(), d.height()); 28 | } 29 | 30 | let display = get_display(i); 31 | let mut capturer = Capturer::new(display).expect("Couldn't begin capture."); 32 | let (w, h) = (capturer.width(), capturer.height()); 33 | 34 | loop { 35 | // Wait until there's a frame. 36 | 37 | let frame = match capturer.frame(Duration::from_millis(0)) { 38 | Ok(frame) => frame, 39 | Err(error) => { 40 | if error.kind() == WouldBlock { 41 | // Keep spinning. 42 | thread::sleep(one_frame); 43 | continue; 44 | } else { 45 | panic!("Error: {}", error); 46 | } 47 | } 48 | }; 49 | let Frame::PixelBuffer(frame) = frame else { 50 | return; 51 | }; 52 | let buffer = frame.data(); 53 | println!("Captured data len: {}, Saving...", buffer.len()); 54 | 55 | // Flip the BGRA image into a RGBA image. 56 | 57 | let mut bitflipped = Vec::with_capacity(w * h * 4); 58 | let stride = buffer.len() / h; 59 | 60 | for y in 0..h { 61 | for x in 0..w { 62 | let i = stride * y + 4 * x; 63 | bitflipped.extend_from_slice(&[buffer[i + 2], buffer[i + 1], buffer[i], 255]); 64 | } 65 | } 66 | 67 | // Save the image. 68 | 69 | let name = format!("screenshot{}_1.png", i); 70 | repng::encode( 71 | File::create(name.clone()).unwrap(), 72 | w as u32, 73 | h as u32, 74 | &bitflipped, 75 | ) 76 | .unwrap(); 77 | 78 | println!("Image saved to `{}`.", name); 79 | break; 80 | } 81 | 82 | drop(capturer); 83 | let display = get_display(i); 84 | let mut capturer = Capturer::new(display).expect("Couldn't begin capture."); 85 | let (w, h) = (capturer.width(), capturer.height()); 86 | 87 | loop { 88 | // Wait until there's a frame. 89 | 90 | let frame = match capturer.frame(Duration::from_millis(0)) { 91 | Ok(frame) => frame, 92 | Err(error) => { 93 | if error.kind() == WouldBlock { 94 | // Keep spinning. 95 | thread::sleep(one_frame); 96 | continue; 97 | } else { 98 | panic!("Error: {}", error); 99 | } 100 | } 101 | }; 102 | let Frame::PixelBuffer(frame) = frame else { 103 | return; 104 | }; 105 | let buffer = frame.data(); 106 | println!("Captured data len: {}, Saving...", buffer.len()); 107 | 108 | let mut raw = Vec::new(); 109 | unsafe { 110 | scrap::ARGBToRAW( 111 | buffer.as_ptr(), 112 | frame.stride()[0] as _, 113 | (&mut raw).as_mut_ptr(), 114 | (w * 3) as _, 115 | w as _, 116 | h as _, 117 | ) 118 | }; 119 | 120 | let mut bitflipped = Vec::with_capacity(w * h * 4); 121 | let stride = raw.len() / h; 122 | 123 | for y in 0..h { 124 | for x in 0..w { 125 | let i = stride * y + 3 * x; 126 | bitflipped.extend_from_slice(&[raw[i], raw[i + 1], raw[i + 2], 255]); 127 | } 128 | } 129 | let name = format!("screenshot{}_2.png", i); 130 | repng::encode( 131 | File::create(name.clone()).unwrap(), 132 | w as u32, 133 | h as u32, 134 | &bitflipped, 135 | ) 136 | .unwrap(); 137 | 138 | println!("Image saved to `{}`.", name); 139 | break; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /libs/scrap/screenshot0_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/libs/scrap/screenshot0_1.png -------------------------------------------------------------------------------- /libs/scrap/src/android/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ffi; 2 | 3 | pub use ffi::*; 4 | -------------------------------------------------------------------------------- /libs/scrap/src/bindings/aom_ffi.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | -------------------------------------------------------------------------------- /libs/scrap/src/bindings/vpx_ffi.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include -------------------------------------------------------------------------------- /libs/scrap/src/bindings/yuv_ffi.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include -------------------------------------------------------------------------------- /libs/scrap/src/common/android.rs: -------------------------------------------------------------------------------- 1 | use crate::android::ffi::*; 2 | use crate::{Frame, Pixfmt}; 3 | use lazy_static::lazy_static; 4 | use serde_json::Value; 5 | use std::collections::HashMap; 6 | use std::sync::Mutex; 7 | use std::{io, time::Duration}; 8 | 9 | lazy_static! { 10 | static ref SCREEN_SIZE: Mutex<(u16, u16, u16)> = Mutex::new((0, 0, 0)); // (width, height, scale) 11 | } 12 | 13 | pub struct Capturer { 14 | display: Display, 15 | rgba: Vec, 16 | saved_raw_data: Vec, // for faster compare and copy 17 | } 18 | 19 | impl Capturer { 20 | pub fn new(display: Display) -> io::Result { 21 | Ok(Capturer { 22 | display, 23 | rgba: Vec::new(), 24 | saved_raw_data: Vec::new(), 25 | }) 26 | } 27 | 28 | pub fn width(&self) -> usize { 29 | self.display.width() as usize 30 | } 31 | 32 | pub fn height(&self) -> usize { 33 | self.display.height() as usize 34 | } 35 | } 36 | 37 | impl crate::TraitCapturer for Capturer { 38 | fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result> { 39 | if let Some(buf) = get_video_raw() { 40 | crate::would_block_if_equal(&mut self.saved_raw_data, buf)?; 41 | // Is it safe to directly return buf without copy? 42 | self.rgba.resize(buf.len(), 0); 43 | unsafe { 44 | std::ptr::copy_nonoverlapping(buf.as_ptr(), self.rgba.as_mut_ptr(), buf.len()) 45 | }; 46 | Ok(Frame::PixelBuffer(PixelBuffer::new( 47 | &self.rgba, 48 | self.width(), 49 | self.height(), 50 | ))) 51 | } else { 52 | return Err(io::ErrorKind::WouldBlock.into()); 53 | } 54 | } 55 | } 56 | 57 | pub struct PixelBuffer<'a> { 58 | data: &'a [u8], 59 | width: usize, 60 | height: usize, 61 | stride: Vec, 62 | } 63 | 64 | impl<'a> PixelBuffer<'a> { 65 | pub fn new(data: &'a [u8], width: usize, height: usize) -> Self { 66 | let stride0 = data.len() / height; 67 | let mut stride = Vec::new(); 68 | stride.push(stride0); 69 | PixelBuffer { 70 | data, 71 | width, 72 | height, 73 | stride, 74 | } 75 | } 76 | } 77 | 78 | impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { 79 | fn data(&self) -> &[u8] { 80 | self.data 81 | } 82 | 83 | fn width(&self) -> usize { 84 | self.width 85 | } 86 | 87 | fn height(&self) -> usize { 88 | self.height 89 | } 90 | 91 | fn stride(&self) -> Vec { 92 | self.stride.clone() 93 | } 94 | 95 | fn pixfmt(&self) -> Pixfmt { 96 | Pixfmt::RGBA 97 | } 98 | } 99 | 100 | pub struct Display { 101 | default: bool, 102 | rect: Rect, 103 | } 104 | 105 | #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] 106 | struct Rect { 107 | pub x: i16, 108 | pub y: i16, 109 | pub w: u16, 110 | pub h: u16, 111 | } 112 | 113 | impl Display { 114 | pub fn primary() -> io::Result { 115 | let mut size = SCREEN_SIZE.lock().unwrap(); 116 | if size.0 == 0 || size.1 == 0 { 117 | *size = get_size().unwrap_or_default(); 118 | } 119 | Ok(Display { 120 | default: true, 121 | rect: Rect { 122 | x: 0, 123 | y: 0, 124 | w: size.0, 125 | h: size.1, 126 | }, 127 | }) 128 | } 129 | 130 | pub fn all() -> io::Result> { 131 | Ok(vec![Display::primary()?]) 132 | } 133 | 134 | pub fn width(&self) -> usize { 135 | self.rect.w as usize 136 | } 137 | 138 | pub fn height(&self) -> usize { 139 | self.rect.h as usize 140 | } 141 | 142 | pub fn origin(&self) -> (i32, i32) { 143 | let r = self.rect; 144 | (r.x as _, r.y as _) 145 | } 146 | 147 | pub fn is_online(&self) -> bool { 148 | true 149 | } 150 | 151 | pub fn is_primary(&self) -> bool { 152 | self.default 153 | } 154 | 155 | pub fn name(&self) -> String { 156 | "Android".into() 157 | } 158 | 159 | pub fn refresh_size() { 160 | let mut size = SCREEN_SIZE.lock().unwrap(); 161 | *size = get_size().unwrap_or_default(); 162 | } 163 | 164 | // Big android screen size will be shrinked, to improve performance when screen-capturing and encoding 165 | // e.g 2280x1080 size will be set to 1140x540, and `scale` is 2 166 | // need to multiply by `4` (2*2) when compute the bitrate 167 | pub fn fix_quality() -> u16 { 168 | let scale = SCREEN_SIZE.lock().unwrap().2; 169 | if scale <= 0 { 170 | 1 171 | } else { 172 | scale * scale 173 | } 174 | } 175 | } 176 | 177 | fn get_size() -> Option<(u16, u16, u16)> { 178 | let res = call_main_service_get_by_name("screen_size").ok()?; 179 | if let Ok(json) = serde_json::from_str::>(&res) { 180 | if let (Some(Value::Number(w)), Some(Value::Number(h)), Some(Value::Number(scale))) = 181 | (json.get("width"), json.get("height"), json.get("scale")) 182 | { 183 | let w = w.as_i64()? as _; 184 | let h = h.as_i64()? as _; 185 | let scale = scale.as_i64()? as _; 186 | return Some((w, h, scale)); 187 | } 188 | } 189 | None 190 | } 191 | -------------------------------------------------------------------------------- /libs/scrap/src/common/dxgi.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "vram")] 2 | use crate::AdapterDevice; 3 | use crate::{common::TraitCapturer, dxgi, Frame, Pixfmt}; 4 | use std::{ 5 | io::{ 6 | self, 7 | ErrorKind::{NotFound, TimedOut, WouldBlock}, 8 | }, 9 | time::Duration, 10 | }; 11 | 12 | pub struct Capturer { 13 | inner: dxgi::Capturer, 14 | width: usize, 15 | height: usize, 16 | } 17 | 18 | impl Capturer { 19 | pub fn new(display: Display) -> io::Result { 20 | let width = display.width(); 21 | let height = display.height(); 22 | let inner = dxgi::Capturer::new(display.0)?; 23 | Ok(Capturer { 24 | inner, 25 | width, 26 | height, 27 | }) 28 | } 29 | 30 | pub fn cancel_gdi(&mut self) { 31 | self.inner.cancel_gdi() 32 | } 33 | 34 | pub fn width(&self) -> usize { 35 | self.width 36 | } 37 | 38 | pub fn height(&self) -> usize { 39 | self.height 40 | } 41 | } 42 | 43 | impl TraitCapturer for Capturer { 44 | fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result> { 45 | match self.inner.frame(timeout.as_millis() as _) { 46 | Ok(frame) => Ok(frame), 47 | Err(ref error) if error.kind() == TimedOut => Err(WouldBlock.into()), 48 | Err(error) => Err(error), 49 | } 50 | } 51 | 52 | fn is_gdi(&self) -> bool { 53 | self.inner.is_gdi() 54 | } 55 | 56 | fn set_gdi(&mut self) -> bool { 57 | self.inner.set_gdi() 58 | } 59 | 60 | #[cfg(feature = "vram")] 61 | fn device(&self) -> AdapterDevice { 62 | self.inner.device() 63 | } 64 | 65 | #[cfg(feature = "vram")] 66 | fn set_output_texture(&mut self, texture: bool) { 67 | self.inner.set_output_texture(texture); 68 | } 69 | } 70 | 71 | pub struct PixelBuffer<'a> { 72 | data: &'a [u8], 73 | width: usize, 74 | height: usize, 75 | stride: Vec, 76 | } 77 | 78 | impl<'a> PixelBuffer<'a> { 79 | pub fn new(data: &'a [u8], width: usize, height: usize) -> Self { 80 | let stride0 = data.len() / height; 81 | let mut stride = Vec::new(); 82 | stride.push(stride0); 83 | PixelBuffer { 84 | data, 85 | width, 86 | height, 87 | stride, 88 | } 89 | } 90 | } 91 | 92 | impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { 93 | fn data(&self) -> &[u8] { 94 | self.data 95 | } 96 | 97 | fn width(&self) -> usize { 98 | self.width 99 | } 100 | 101 | fn height(&self) -> usize { 102 | self.height 103 | } 104 | 105 | fn stride(&self) -> Vec { 106 | self.stride.clone() 107 | } 108 | 109 | fn pixfmt(&self) -> Pixfmt { 110 | Pixfmt::BGRA 111 | } 112 | } 113 | 114 | pub struct Display(dxgi::Display); 115 | 116 | impl Display { 117 | pub fn primary() -> io::Result { 118 | // not implemented yet 119 | Err(NotFound.into()) 120 | } 121 | 122 | pub fn all() -> io::Result> { 123 | let displays_gdi = dxgi::Displays::get_from_gdi() 124 | .drain(..) 125 | .map(Display) 126 | .collect::>(); 127 | 128 | let displays_dxgi = Self::all_().unwrap_or(Default::default()); 129 | 130 | // Return gdi displays if dxgi is not supported 131 | if displays_dxgi.is_empty() { 132 | println!("Display got from gdi"); 133 | return Ok(displays_gdi); 134 | } 135 | 136 | // Return dxgi displays if length is not equal 137 | if displays_dxgi.len() != displays_gdi.len() { 138 | return Ok(displays_dxgi); 139 | } 140 | 141 | // Check if names are equal 142 | let names_gdi = displays_gdi.iter().map(|d| d.name()).collect::>(); 143 | let names_dxgi = displays_dxgi.iter().map(|d| d.name()).collect::>(); 144 | for name in names_gdi.iter() { 145 | if !names_dxgi.contains(name) { 146 | return Ok(displays_dxgi); 147 | } 148 | } 149 | 150 | // Reorder displays from dxgi 151 | let mut displays_dxgi = displays_dxgi; 152 | let mut displays_dxgi_ordered = Vec::new(); 153 | for name in names_gdi.iter() { 154 | let pos = match displays_dxgi.iter().position(|d| d.name() == *name) { 155 | Some(pos) => pos, 156 | None => { 157 | // unreachable! 158 | 0 159 | } 160 | }; 161 | displays_dxgi_ordered.push(displays_dxgi.remove(pos)); 162 | } 163 | 164 | Ok(displays_dxgi_ordered) 165 | } 166 | 167 | fn all_() -> io::Result> { 168 | Ok(dxgi::Displays::new()?.map(Display).collect::>()) 169 | } 170 | 171 | pub fn width(&self) -> usize { 172 | self.0.width() as usize 173 | } 174 | 175 | pub fn height(&self) -> usize { 176 | self.0.height() as usize 177 | } 178 | 179 | pub fn name(&self) -> String { 180 | use std::ffi::OsString; 181 | use std::os::windows::prelude::*; 182 | OsString::from_wide(self.0.name()) 183 | .to_string_lossy() 184 | .to_string() 185 | } 186 | 187 | pub fn is_online(&self) -> bool { 188 | self.0.is_online() 189 | } 190 | 191 | pub fn origin(&self) -> (i32, i32) { 192 | self.0.origin() 193 | } 194 | 195 | pub fn is_primary(&self) -> bool { 196 | // https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-devmodea 197 | self.origin() == (0, 0) 198 | } 199 | 200 | #[cfg(feature = "vram")] 201 | pub fn adapter_luid(&self) -> Option { 202 | self.0.adapter_luid() 203 | } 204 | } 205 | 206 | pub struct CapturerMag { 207 | inner: dxgi::mag::CapturerMag, 208 | data: Vec, 209 | } 210 | 211 | impl CapturerMag { 212 | pub fn is_supported() -> bool { 213 | dxgi::mag::CapturerMag::is_supported() 214 | } 215 | 216 | pub fn new(origin: (i32, i32), width: usize, height: usize) -> io::Result { 217 | Ok(CapturerMag { 218 | inner: dxgi::mag::CapturerMag::new(origin, width, height)?, 219 | data: Vec::new(), 220 | }) 221 | } 222 | 223 | pub fn exclude(&mut self, cls: &str, name: &str) -> io::Result { 224 | self.inner.exclude(cls, name) 225 | } 226 | // ((x, y), w, h) 227 | pub fn get_rect(&self) -> ((i32, i32), usize, usize) { 228 | self.inner.get_rect() 229 | } 230 | } 231 | 232 | impl TraitCapturer for CapturerMag { 233 | fn frame<'a>(&'a mut self, _timeout_ms: Duration) -> io::Result> { 234 | self.inner.frame(&mut self.data)?; 235 | Ok(Frame::PixelBuffer(PixelBuffer::new( 236 | &self.data, 237 | self.inner.get_rect().1, 238 | self.inner.get_rect().2, 239 | ))) 240 | } 241 | 242 | fn is_gdi(&self) -> bool { 243 | false 244 | } 245 | 246 | fn set_gdi(&mut self) -> bool { 247 | false 248 | } 249 | 250 | #[cfg(feature = "vram")] 251 | fn device(&self) -> AdapterDevice { 252 | AdapterDevice::default() 253 | } 254 | 255 | #[cfg(feature = "vram")] 256 | fn set_output_texture(&mut self, _texture: bool) {} 257 | } 258 | -------------------------------------------------------------------------------- /libs/scrap/src/common/linux.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{ 3 | wayland, 4 | x11::{self}, 5 | TraitCapturer, 6 | }, 7 | Frame, 8 | }; 9 | use std::{io, time::Duration}; 10 | 11 | pub enum Capturer { 12 | X11(x11::Capturer), 13 | WAYLAND(wayland::Capturer), 14 | } 15 | 16 | impl Capturer { 17 | pub fn new(display: Display) -> io::Result { 18 | Ok(match display { 19 | Display::X11(d) => Capturer::X11(x11::Capturer::new(d)?), 20 | Display::WAYLAND(d) => Capturer::WAYLAND(wayland::Capturer::new(d)?), 21 | }) 22 | } 23 | 24 | pub fn width(&self) -> usize { 25 | match self { 26 | Capturer::X11(d) => d.width(), 27 | Capturer::WAYLAND(d) => d.width(), 28 | } 29 | } 30 | 31 | pub fn height(&self) -> usize { 32 | match self { 33 | Capturer::X11(d) => d.height(), 34 | Capturer::WAYLAND(d) => d.height(), 35 | } 36 | } 37 | } 38 | 39 | impl TraitCapturer for Capturer { 40 | fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result> { 41 | match self { 42 | Capturer::X11(d) => d.frame(timeout), 43 | Capturer::WAYLAND(d) => d.frame(timeout), 44 | } 45 | } 46 | } 47 | 48 | pub enum Display { 49 | X11(x11::Display), 50 | WAYLAND(wayland::Display), 51 | } 52 | 53 | impl Display { 54 | pub fn primary() -> io::Result { 55 | Ok(if super::is_x11() { 56 | Display::X11(x11::Display::primary()?) 57 | } else { 58 | Display::WAYLAND(wayland::Display::primary()?) 59 | }) 60 | } 61 | 62 | pub fn all() -> io::Result> { 63 | Ok(if super::is_x11() { 64 | x11::Display::all()? 65 | .drain(..) 66 | .map(|x| Display::X11(x)) 67 | .collect() 68 | } else { 69 | wayland::Display::all()? 70 | .drain(..) 71 | .map(|x| Display::WAYLAND(x)) 72 | .collect() 73 | }) 74 | } 75 | 76 | pub fn width(&self) -> usize { 77 | match self { 78 | Display::X11(d) => d.width(), 79 | Display::WAYLAND(d) => d.width(), 80 | } 81 | } 82 | 83 | pub fn height(&self) -> usize { 84 | match self { 85 | Display::X11(d) => d.height(), 86 | Display::WAYLAND(d) => d.height(), 87 | } 88 | } 89 | 90 | pub fn origin(&self) -> (i32, i32) { 91 | match self { 92 | Display::X11(d) => d.origin(), 93 | Display::WAYLAND(d) => d.origin(), 94 | } 95 | } 96 | 97 | pub fn is_online(&self) -> bool { 98 | match self { 99 | Display::X11(d) => d.is_online(), 100 | Display::WAYLAND(d) => d.is_online(), 101 | } 102 | } 103 | 104 | pub fn is_primary(&self) -> bool { 105 | match self { 106 | Display::X11(d) => d.is_primary(), 107 | Display::WAYLAND(d) => d.is_primary(), 108 | } 109 | } 110 | 111 | pub fn name(&self) -> String { 112 | match self { 113 | Display::X11(d) => d.name(), 114 | Display::WAYLAND(d) => d.name(), 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /libs/scrap/src/common/mediacodec.rs: -------------------------------------------------------------------------------- 1 | use hbb_common::{anyhow::Error, bail, log, ResultType}; 2 | use ndk::media::media_codec::{MediaCodec, MediaCodecDirection, MediaFormat}; 3 | use std::ops::Deref; 4 | use std::{ 5 | io::Write, 6 | sync::atomic::{AtomicBool, Ordering}, 7 | time::Duration, 8 | }; 9 | 10 | use crate::ImageFormat; 11 | use crate::{ 12 | codec::{EncoderApi, EncoderCfg}, 13 | CodecFormat, I420ToABGR, I420ToARGB, ImageRgb, 14 | }; 15 | 16 | /// MediaCodec mime type name 17 | const H264_MIME_TYPE: &str = "video/avc"; 18 | const H265_MIME_TYPE: &str = "video/hevc"; 19 | // const VP8_MIME_TYPE: &str = "video/x-vnd.on2.vp8"; 20 | // const VP9_MIME_TYPE: &str = "video/x-vnd.on2.vp9"; 21 | 22 | // TODO MediaCodecEncoder 23 | 24 | pub static H264_DECODER_SUPPORT: AtomicBool = AtomicBool::new(false); 25 | pub static H265_DECODER_SUPPORT: AtomicBool = AtomicBool::new(false); 26 | 27 | pub struct MediaCodecDecoder { 28 | decoder: MediaCodec, 29 | name: String, 30 | } 31 | 32 | impl Deref for MediaCodecDecoder { 33 | type Target = MediaCodec; 34 | 35 | fn deref(&self) -> &Self::Target { 36 | &self.decoder 37 | } 38 | } 39 | 40 | impl MediaCodecDecoder { 41 | pub fn new(format: CodecFormat) -> Option { 42 | match format { 43 | CodecFormat::H264 => create_media_codec(H264_MIME_TYPE, MediaCodecDirection::Decoder), 44 | CodecFormat::H265 => create_media_codec(H265_MIME_TYPE, MediaCodecDirection::Decoder), 45 | _ => { 46 | log::error!("Unsupported codec format: {}", format); 47 | None 48 | } 49 | } 50 | } 51 | 52 | // rgb [in/out] fmt and stride must be set in ImageRgb 53 | pub fn decode(&mut self, data: &[u8], rgb: &mut ImageRgb) -> ResultType { 54 | // take dst_stride into account please 55 | let dst_stride = rgb.stride(); 56 | match self.dequeue_input_buffer(Duration::from_millis(10))? { 57 | Some(mut input_buffer) => { 58 | let mut buf = input_buffer.buffer_mut(); 59 | if data.len() > buf.len() { 60 | log::error!("Failed to decode, the input data size is bigger than input buf"); 61 | bail!("The input data size is bigger than input buf"); 62 | } 63 | buf.write_all(&data)?; 64 | self.queue_input_buffer(input_buffer, 0, data.len(), 0, 0)?; 65 | } 66 | None => { 67 | log::debug!("Failed to dequeue_input_buffer: No available input_buffer"); 68 | } 69 | }; 70 | 71 | return match self.dequeue_output_buffer(Duration::from_millis(100))? { 72 | Some(output_buffer) => { 73 | let res_format = self.output_format(); 74 | let w = res_format 75 | .i32("width") 76 | .ok_or(Error::msg("Failed to dequeue_output_buffer, width is None"))? 77 | as usize; 78 | let h = res_format.i32("height").ok_or(Error::msg( 79 | "Failed to dequeue_output_buffer, height is None", 80 | ))? as usize; 81 | let stride = res_format.i32("stride").ok_or(Error::msg( 82 | "Failed to dequeue_output_buffer, stride is None", 83 | ))?; 84 | let buf = output_buffer.buffer(); 85 | let bps = 4; 86 | let u = buf.len() * 2 / 3; 87 | let v = buf.len() * 5 / 6; 88 | rgb.raw.resize(h * w * bps, 0); 89 | let y_ptr = buf.as_ptr(); 90 | let u_ptr = buf[u..].as_ptr(); 91 | let v_ptr = buf[v..].as_ptr(); 92 | unsafe { 93 | match rgb.fmt() { 94 | ImageFormat::ARGB => { 95 | I420ToARGB( 96 | y_ptr, 97 | stride, 98 | u_ptr, 99 | stride / 2, 100 | v_ptr, 101 | stride / 2, 102 | rgb.raw.as_mut_ptr(), 103 | (w * bps) as _, 104 | w as _, 105 | h as _, 106 | ); 107 | } 108 | ImageFormat::ARGB => { 109 | I420ToABGR( 110 | y_ptr, 111 | stride, 112 | u_ptr, 113 | stride / 2, 114 | v_ptr, 115 | stride / 2, 116 | rgb.raw.as_mut_ptr(), 117 | (w * bps) as _, 118 | w as _, 119 | h as _, 120 | ); 121 | } 122 | _ => { 123 | bail!("Unsupported image format"); 124 | } 125 | } 126 | } 127 | self.release_output_buffer(output_buffer, false)?; 128 | Ok(true) 129 | } 130 | None => { 131 | log::debug!("Failed to dequeue_output: No available dequeue_output"); 132 | Ok(false) 133 | } 134 | }; 135 | } 136 | } 137 | 138 | fn create_media_codec(name: &str, direction: MediaCodecDirection) -> Option { 139 | let codec = MediaCodec::from_decoder_type(name)?; 140 | let media_format = MediaFormat::new(); 141 | media_format.set_str("mime", name); 142 | media_format.set_i32("width", 0); 143 | media_format.set_i32("height", 0); 144 | media_format.set_i32("color-format", 19); // COLOR_FormatYUV420Planar 145 | if let Err(e) = codec.configure(&media_format, None, direction) { 146 | log::error!("Failed to init decoder: {:?}", e); 147 | return None; 148 | }; 149 | log::error!("decoder init success"); 150 | if let Err(e) = codec.start() { 151 | log::error!("Failed to start decoder: {:?}", e); 152 | return None; 153 | }; 154 | log::debug!("Init decoder successed!: {:?}", name); 155 | return Some(MediaCodecDecoder { 156 | decoder: codec, 157 | name: name.to_owned(), 158 | }); 159 | } 160 | 161 | pub fn check_mediacodec() { 162 | std::thread::spawn(move || { 163 | // check decoders 164 | let decoders = MediaCodecDecoder::new_decoders(); 165 | H264_DECODER_SUPPORT.swap(decoders.h264.is_some(), Ordering::SeqCst); 166 | H265_DECODER_SUPPORT.swap(decoders.h265.is_some(), Ordering::SeqCst); 167 | decoders.h264.map(|d| d.stop()); 168 | decoders.h265.map(|d| d.stop()); 169 | // TODO encoders 170 | }); 171 | } 172 | -------------------------------------------------------------------------------- /libs/scrap/src/common/quartz.rs: -------------------------------------------------------------------------------- 1 | use crate::{quartz, Frame, Pixfmt}; 2 | use std::marker::PhantomData; 3 | use std::sync::{Arc, Mutex, TryLockError}; 4 | use std::{io, mem}; 5 | 6 | pub struct Capturer { 7 | inner: quartz::Capturer, 8 | frame: Arc>>, 9 | saved_raw_data: Vec, // for faster compare and copy 10 | } 11 | 12 | impl Capturer { 13 | pub fn new(display: Display) -> io::Result { 14 | let frame = Arc::new(Mutex::new(None)); 15 | 16 | let f = frame.clone(); 17 | let inner = quartz::Capturer::new( 18 | display.0, 19 | display.width(), 20 | display.height(), 21 | quartz::PixelFormat::Argb8888, 22 | Default::default(), 23 | move |inner| { 24 | if let Ok(mut f) = f.lock() { 25 | *f = Some(inner); 26 | } 27 | }, 28 | ) 29 | .map_err(|_| io::Error::from(io::ErrorKind::Other))?; 30 | 31 | Ok(Capturer { 32 | inner, 33 | frame, 34 | saved_raw_data: Vec::new(), 35 | }) 36 | } 37 | 38 | pub fn width(&self) -> usize { 39 | self.inner.width() 40 | } 41 | 42 | pub fn height(&self) -> usize { 43 | self.inner.height() 44 | } 45 | } 46 | 47 | impl crate::TraitCapturer for Capturer { 48 | fn frame<'a>(&'a mut self, _timeout_ms: std::time::Duration) -> io::Result> { 49 | match self.frame.try_lock() { 50 | Ok(mut handle) => { 51 | let mut frame = None; 52 | mem::swap(&mut frame, &mut handle); 53 | 54 | match frame { 55 | Some(mut frame) => { 56 | crate::would_block_if_equal(&mut self.saved_raw_data, frame.inner())?; 57 | frame.surface_to_bgra(self.height()); 58 | Ok(Frame::PixelBuffer(PixelBuffer { 59 | frame, 60 | data: PhantomData, 61 | width: self.width(), 62 | height: self.height(), 63 | })) 64 | } 65 | 66 | None => Err(io::ErrorKind::WouldBlock.into()), 67 | } 68 | } 69 | 70 | Err(TryLockError::WouldBlock) => Err(io::ErrorKind::WouldBlock.into()), 71 | 72 | Err(TryLockError::Poisoned(..)) => Err(io::ErrorKind::Other.into()), 73 | } 74 | } 75 | } 76 | 77 | pub struct PixelBuffer<'a> { 78 | frame: quartz::Frame, 79 | data: PhantomData<&'a [u8]>, 80 | width: usize, 81 | height: usize, 82 | } 83 | 84 | impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { 85 | fn data(&self) -> &[u8] { 86 | &*self.frame 87 | } 88 | 89 | fn width(&self) -> usize { 90 | self.width 91 | } 92 | 93 | fn height(&self) -> usize { 94 | self.height 95 | } 96 | 97 | fn stride(&self) -> Vec { 98 | let mut v = Vec::new(); 99 | v.push(self.frame.stride()); 100 | v 101 | } 102 | 103 | fn pixfmt(&self) -> Pixfmt { 104 | Pixfmt::BGRA 105 | } 106 | } 107 | 108 | pub struct Display(quartz::Display); 109 | 110 | impl Display { 111 | pub fn primary() -> io::Result { 112 | Ok(Display(quartz::Display::primary())) 113 | } 114 | 115 | pub fn all() -> io::Result> { 116 | Ok(quartz::Display::online() 117 | .map_err(|_| io::Error::from(io::ErrorKind::Other))? 118 | .into_iter() 119 | .map(Display) 120 | .collect()) 121 | } 122 | 123 | pub fn width(&self) -> usize { 124 | self.0.width() 125 | } 126 | 127 | pub fn height(&self) -> usize { 128 | self.0.height() 129 | } 130 | 131 | pub fn scale(&self) -> f64 { 132 | self.0.scale() 133 | } 134 | 135 | pub fn name(&self) -> String { 136 | self.0.id().to_string() 137 | } 138 | 139 | pub fn is_online(&self) -> bool { 140 | self.0.is_online() 141 | } 142 | 143 | pub fn origin(&self) -> (i32, i32) { 144 | let o = self.0.bounds().origin; 145 | (o.x as _, o.y as _) 146 | } 147 | 148 | pub fn is_primary(&self) -> bool { 149 | self.0.is_primary() 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /libs/scrap/src/common/vpx.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | #![allow(non_snake_case)] 3 | #![allow(non_upper_case_globals)] 4 | #![allow(improper_ctypes)] 5 | #![allow(dead_code)] 6 | 7 | impl Default for vpx_codec_enc_cfg { 8 | fn default() -> Self { 9 | unsafe { std::mem::zeroed() } 10 | } 11 | } 12 | 13 | impl Default for vpx_codec_ctx { 14 | fn default() -> Self { 15 | unsafe { std::mem::zeroed() } 16 | } 17 | } 18 | 19 | impl Default for vpx_image_t { 20 | fn default() -> Self { 21 | unsafe { std::mem::zeroed() } 22 | } 23 | } 24 | 25 | include!(concat!(env!("OUT_DIR"), "/vpx_ffi.rs")); 26 | -------------------------------------------------------------------------------- /libs/scrap/src/common/wayland.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | wayland::{capturable::*, *}, 3 | Frame, TraitCapturer, 4 | }; 5 | use std::{io, sync::RwLock, time::Duration}; 6 | 7 | use super::x11::PixelBuffer; 8 | 9 | pub struct Capturer(Display, Box, Vec); 10 | 11 | 12 | lazy_static::lazy_static! { 13 | static ref MAP_ERR: RwLock io::Error>> = Default::default(); 14 | } 15 | 16 | pub fn set_map_err(f: fn(err: String) -> io::Error) { 17 | *MAP_ERR.write().unwrap() = Some(f); 18 | } 19 | 20 | fn map_err(err: E) -> io::Error { 21 | if let Some(f) = *MAP_ERR.read().unwrap() { 22 | f(err.to_string()) 23 | } else { 24 | io::Error::new(io::ErrorKind::Other, err.to_string()) 25 | } 26 | } 27 | 28 | impl Capturer { 29 | pub fn new(display: Display) -> io::Result { 30 | let r = display.0.recorder(false).map_err(map_err)?; 31 | Ok(Capturer(display, r, Default::default())) 32 | } 33 | 34 | pub fn width(&self) -> usize { 35 | self.0.width() 36 | } 37 | 38 | pub fn height(&self) -> usize { 39 | self.0.height() 40 | } 41 | } 42 | 43 | impl TraitCapturer for Capturer { 44 | fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result> { 45 | match self.1.capture(timeout.as_millis() as _).map_err(map_err)? { 46 | PixelProvider::BGR0(w, h, x) => Ok(Frame::PixelBuffer(PixelBuffer::new( 47 | x, 48 | crate::Pixfmt::BGRA, 49 | w, 50 | h, 51 | ))), 52 | PixelProvider::RGB0(w, h, x) => Ok(Frame::PixelBuffer(PixelBuffer::new( 53 | x, 54 | crate::Pixfmt::RGBA, 55 | w, 56 | h, 57 | ))), 58 | PixelProvider::NONE => Err(std::io::ErrorKind::WouldBlock.into()), 59 | _ => Err(map_err("Invalid data")), 60 | } 61 | } 62 | } 63 | 64 | pub struct Display(pipewire::PipeWireCapturable); 65 | 66 | impl Display { 67 | pub fn primary() -> io::Result { 68 | let mut all = Display::all()?; 69 | if all.is_empty() { 70 | return Err(io::ErrorKind::NotFound.into()); 71 | } 72 | Ok(all.remove(0)) 73 | } 74 | 75 | pub fn all() -> io::Result> { 76 | Ok(pipewire::get_capturables() 77 | .map_err(map_err)? 78 | .drain(..) 79 | .map(|x| Display(x)) 80 | .collect()) 81 | } 82 | 83 | pub fn width(&self) -> usize { 84 | self.0.size.0 85 | } 86 | 87 | pub fn height(&self) -> usize { 88 | self.0.size.1 89 | } 90 | 91 | pub fn origin(&self) -> (i32, i32) { 92 | self.0.position 93 | } 94 | 95 | pub fn is_online(&self) -> bool { 96 | true 97 | } 98 | 99 | pub fn is_primary(&self) -> bool { 100 | false 101 | } 102 | 103 | pub fn name(&self) -> String { 104 | "".to_owned() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /libs/scrap/src/common/x11.rs: -------------------------------------------------------------------------------- 1 | use crate::{common::TraitCapturer, x11, Frame, Pixfmt, TraitPixelBuffer}; 2 | use std::{io, time::Duration}; 3 | 4 | pub struct Capturer(x11::Capturer); 5 | 6 | pub const IS_CURSOR_EMBEDDED: bool = false; 7 | 8 | impl Capturer { 9 | pub fn new(display: Display) -> io::Result { 10 | x11::Capturer::new(display.0).map(Capturer) 11 | } 12 | 13 | pub fn width(&self) -> usize { 14 | self.0.display().rect().w as usize 15 | } 16 | 17 | pub fn height(&self) -> usize { 18 | self.0.display().rect().h as usize 19 | } 20 | } 21 | 22 | impl TraitCapturer for Capturer { 23 | fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result> { 24 | let width = self.width(); 25 | let height = self.height(); 26 | Ok(Frame::PixelBuffer(PixelBuffer::new( 27 | self.0.frame()?, 28 | Pixfmt::BGRA, 29 | width, 30 | height, 31 | ))) 32 | } 33 | } 34 | 35 | pub struct PixelBuffer<'a> { 36 | data: &'a [u8], 37 | pixfmt: Pixfmt, 38 | width: usize, 39 | height: usize, 40 | stride: Vec, 41 | } 42 | 43 | impl<'a> PixelBuffer<'a> { 44 | pub fn new(data: &'a [u8], pixfmt: Pixfmt, width: usize, height: usize) -> Self { 45 | let stride0 = data.len() / height; 46 | let mut stride = Vec::new(); 47 | stride.push(stride0); 48 | Self { 49 | data, 50 | pixfmt, 51 | width, 52 | height, 53 | stride, 54 | } 55 | } 56 | } 57 | 58 | impl<'a> TraitPixelBuffer for PixelBuffer<'a> { 59 | fn data(&self) -> &[u8] { 60 | self.data 61 | } 62 | 63 | fn width(&self) -> usize { 64 | self.width 65 | } 66 | 67 | fn height(&self) -> usize { 68 | self.height 69 | } 70 | 71 | fn stride(&self) -> Vec { 72 | self.stride.clone() 73 | } 74 | 75 | fn pixfmt(&self) -> crate::Pixfmt { 76 | self.pixfmt 77 | } 78 | } 79 | 80 | pub struct Display(x11::Display); 81 | 82 | impl Display { 83 | pub fn primary() -> io::Result { 84 | let server = match x11::Server::default() { 85 | Ok(server) => server, 86 | Err(_) => return Err(io::ErrorKind::ConnectionRefused.into()), 87 | }; 88 | 89 | let mut displays = x11::Server::displays(server); 90 | let mut best = displays.next(); 91 | if best.as_ref().map(|x| x.is_default()) == Some(false) { 92 | best = displays.find(|x| x.is_default()).or(best); 93 | } 94 | 95 | match best { 96 | Some(best) => Ok(Display(best)), 97 | None => Err(io::ErrorKind::NotFound.into()), 98 | } 99 | } 100 | 101 | pub fn all() -> io::Result> { 102 | let server = match x11::Server::default() { 103 | Ok(server) => server, 104 | Err(_) => return Err(io::ErrorKind::ConnectionRefused.into()), 105 | }; 106 | 107 | Ok(x11::Server::displays(server).map(Display).collect()) 108 | } 109 | 110 | pub fn width(&self) -> usize { 111 | self.0.rect().w as usize 112 | } 113 | 114 | pub fn height(&self) -> usize { 115 | self.0.rect().h as usize 116 | } 117 | 118 | pub fn origin(&self) -> (i32, i32) { 119 | let r = self.0.rect(); 120 | (r.x as _, r.y as _) 121 | } 122 | 123 | pub fn is_online(&self) -> bool { 124 | true 125 | } 126 | 127 | pub fn is_primary(&self) -> bool { 128 | self.0.is_default() 129 | } 130 | 131 | pub fn name(&self) -> String { 132 | self.0.name() 133 | } 134 | 135 | pub fn get_shm_status(&self) -> Result<(), x11::Error> { 136 | self.0.server().get_shm_status() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /libs/scrap/src/dxgi/gdi.rs: -------------------------------------------------------------------------------- 1 | use std::mem::size_of; 2 | use winapi::{ 3 | shared::windef::{HBITMAP, HDC}, 4 | um::wingdi::{ 5 | BitBlt, 6 | CreateCompatibleBitmap, 7 | CreateCompatibleDC, 8 | CreateDCW, 9 | DeleteDC, 10 | DeleteObject, 11 | GetDIBits, 12 | SelectObject, 13 | BITMAPINFO, 14 | BITMAPINFOHEADER, 15 | BI_RGB, 16 | CAPTUREBLT, 17 | DIB_RGB_COLORS, //CAPTUREBLT, 18 | HGDI_ERROR, 19 | RGBQUAD, 20 | SRCCOPY, 21 | }, 22 | }; 23 | 24 | const PIXEL_WIDTH: i32 = 4; 25 | 26 | pub struct CapturerGDI { 27 | screen_dc: HDC, 28 | dc: HDC, 29 | bmp: HBITMAP, 30 | width: i32, 31 | height: i32, 32 | } 33 | 34 | impl CapturerGDI { 35 | pub fn new(name: &[u16], width: i32, height: i32) -> Result> { 36 | /* or Enumerate monitors with EnumDisplayMonitors, 37 | https://stackoverflow.com/questions/34987695/how-can-i-get-an-hmonitor-handle-from-a-display-device-name 38 | #[no_mangle] 39 | pub extern "C" fn callback(m: HMONITOR, dc: HDC, rect: LPRECT, lp: LPARAM) -> BOOL {} 40 | */ 41 | /* 42 | shared::windef::HMONITOR, 43 | winuser::{GetMonitorInfoW, GetSystemMetrics, MONITORINFOEXW}, 44 | let mut mi: MONITORINFOEXW = std::mem::MaybeUninit::uninit().assume_init(); 45 | mi.cbSize = size_of::() as _; 46 | if GetMonitorInfoW(m, &mut mi as *mut MONITORINFOEXW as _) == 0 { 47 | return Err(format!("Failed to get monitor information of: {:?}", m).into()); 48 | } 49 | */ 50 | unsafe { 51 | if name.is_empty() { 52 | return Err("Empty display name".into()); 53 | } 54 | let screen_dc = CreateDCW(&name[0], 0 as _, 0 as _, 0 as _); 55 | if screen_dc.is_null() { 56 | return Err("Failed to create dc from monitor name".into()); 57 | } 58 | 59 | // Create a Windows Bitmap, and copy the bits into it 60 | let dc = CreateCompatibleDC(screen_dc); 61 | if dc.is_null() { 62 | DeleteDC(screen_dc); 63 | return Err("Can't get a Windows display".into()); 64 | } 65 | 66 | let bmp = CreateCompatibleBitmap(screen_dc, width, height); 67 | if bmp.is_null() { 68 | DeleteDC(screen_dc); 69 | DeleteDC(dc); 70 | return Err("Can't create a Windows buffer".into()); 71 | } 72 | 73 | let res = SelectObject(dc, bmp as _); 74 | if res.is_null() || res == HGDI_ERROR { 75 | DeleteDC(screen_dc); 76 | DeleteDC(dc); 77 | DeleteObject(bmp as _); 78 | return Err("Can't select Windows buffer".into()); 79 | } 80 | Ok(Self { 81 | screen_dc, 82 | dc, 83 | bmp, 84 | width, 85 | height, 86 | }) 87 | } 88 | } 89 | 90 | pub fn frame(&self, data: &mut Vec) -> Result<(), Box> { 91 | unsafe { 92 | let res = BitBlt( 93 | self.dc, 94 | 0, 95 | 0, 96 | self.width, 97 | self.height, 98 | self.screen_dc, 99 | 0, 100 | 0, 101 | SRCCOPY | CAPTUREBLT, // CAPTUREBLT enable layered window but also make cursor blinking 102 | ); 103 | if res == 0 { 104 | return Err("Failed to copy screen to Windows buffer".into()); 105 | } 106 | 107 | let stride = self.width * PIXEL_WIDTH; 108 | let size: usize = (stride * self.height) as usize; 109 | let mut data1: Vec = Vec::with_capacity(size); 110 | data1.set_len(size); 111 | data.resize(size, 0); 112 | 113 | let mut bmi = BITMAPINFO { 114 | bmiHeader: BITMAPINFOHEADER { 115 | biSize: size_of::() as _, 116 | biWidth: self.width as _, 117 | biHeight: self.height as _, 118 | biPlanes: 1, 119 | biBitCount: (8 * PIXEL_WIDTH) as _, 120 | biCompression: BI_RGB, 121 | biSizeImage: (self.width * self.height * PIXEL_WIDTH) as _, 122 | biXPelsPerMeter: 0, 123 | biYPelsPerMeter: 0, 124 | biClrUsed: 0, 125 | biClrImportant: 0, 126 | }, 127 | bmiColors: [RGBQUAD { 128 | rgbBlue: 0, 129 | rgbGreen: 0, 130 | rgbRed: 0, 131 | rgbReserved: 0, 132 | }], 133 | }; 134 | 135 | // copy bits into Vec 136 | let res = GetDIBits( 137 | self.dc, 138 | self.bmp, 139 | 0, 140 | self.height as _, 141 | &mut data[0] as *mut u8 as _, 142 | &mut bmi as _, 143 | DIB_RGB_COLORS, 144 | ); 145 | if res == 0 { 146 | return Err("GetDIBits failed".into()); 147 | } 148 | crate::common::ARGBMirror( 149 | data.as_ptr(), 150 | stride, 151 | data1.as_mut_ptr(), 152 | stride, 153 | self.width, 154 | self.height, 155 | ); 156 | crate::common::ARGBRotate( 157 | data1.as_ptr(), 158 | stride, 159 | data.as_mut_ptr(), 160 | stride, 161 | self.width, 162 | self.height, 163 | crate::RotationMode::kRotate180, 164 | ); 165 | Ok(()) 166 | } 167 | } 168 | } 169 | 170 | impl Drop for CapturerGDI { 171 | fn drop(&mut self) { 172 | unsafe { 173 | DeleteDC(self.screen_dc); 174 | DeleteDC(self.dc); 175 | DeleteObject(self.bmp as _); 176 | } 177 | } 178 | } 179 | 180 | #[cfg(test)] 181 | mod tests { 182 | use super::super::*; 183 | use super::*; 184 | #[test] 185 | fn test() { 186 | match Displays::new().unwrap().next() { 187 | Some(d) => { 188 | let w = d.width(); 189 | let h = d.height(); 190 | let c = CapturerGDI::new(d.name(), w, h).unwrap(); 191 | let mut data = Vec::new(); 192 | c.frame(&mut data).unwrap(); 193 | let mut bitflipped = Vec::with_capacity((w * h * 4) as usize); 194 | for y in 0..h { 195 | for x in 0..w { 196 | let i = (w * 4 * y + 4 * x) as usize; 197 | bitflipped.extend_from_slice(&[data[i + 2], data[i + 1], data[i], 255]); 198 | } 199 | } 200 | repng::encode( 201 | std::fs::File::create("gdi_screen.png").unwrap(), 202 | d.width() as u32, 203 | d.height() as u32, 204 | &bitflipped, 205 | ) 206 | .unwrap(); 207 | } 208 | _ => { 209 | assert!(false); 210 | } 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /libs/scrap/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(quartz)] 2 | extern crate block; 3 | #[macro_use] 4 | extern crate cfg_if; 5 | pub use hbb_common::libc; 6 | #[cfg(dxgi)] 7 | extern crate winapi; 8 | 9 | pub use common::*; 10 | 11 | #[cfg(quartz)] 12 | pub mod quartz; 13 | 14 | #[cfg(x11)] 15 | pub mod x11; 16 | 17 | #[cfg(all(x11, feature = "wayland"))] 18 | pub mod wayland; 19 | 20 | #[cfg(dxgi)] 21 | pub mod dxgi; 22 | 23 | #[cfg(target_os = "android")] 24 | pub mod android; 25 | 26 | mod common; 27 | -------------------------------------------------------------------------------- /libs/scrap/src/quartz/capturer.rs: -------------------------------------------------------------------------------- 1 | use std::ptr; 2 | 3 | use block::{Block, ConcreteBlock}; 4 | use hbb_common::libc::c_void; 5 | use std::sync::{Arc, Mutex}; 6 | 7 | use super::config::Config; 8 | use super::display::Display; 9 | use super::ffi::*; 10 | use super::frame::Frame; 11 | 12 | pub struct Capturer { 13 | stream: CGDisplayStreamRef, 14 | queue: DispatchQueue, 15 | 16 | width: usize, 17 | height: usize, 18 | format: PixelFormat, 19 | display: Display, 20 | stopped: Arc>, 21 | } 22 | 23 | impl Capturer { 24 | pub fn new( 25 | display: Display, 26 | width: usize, 27 | height: usize, 28 | format: PixelFormat, 29 | config: Config, 30 | handler: F, 31 | ) -> Result { 32 | let stopped = Arc::new(Mutex::new(false)); 33 | let cloned_stopped = stopped.clone(); 34 | let handler: FrameAvailableHandler = ConcreteBlock::new(move |status, _, surface, _| { 35 | use self::CGDisplayStreamFrameStatus::*; 36 | if status == Stopped { 37 | let mut lock = cloned_stopped.lock().unwrap(); 38 | *lock = true; 39 | return; 40 | } 41 | if status == FrameComplete { 42 | handler(unsafe { Frame::new(surface) }); 43 | } 44 | }) 45 | .copy(); 46 | 47 | let queue = unsafe { 48 | dispatch_queue_create( 49 | b"quadrupleslap.scrap\0".as_ptr() as *const i8, 50 | ptr::null_mut(), 51 | ) 52 | }; 53 | 54 | let stream = unsafe { 55 | let config = config.build(); 56 | let stream = CGDisplayStreamCreateWithDispatchQueue( 57 | display.id(), 58 | width, 59 | height, 60 | format, 61 | config, 62 | queue, 63 | &*handler as *const Block<_, _> as *const c_void, 64 | ); 65 | CFRelease(config); 66 | stream 67 | }; 68 | 69 | match unsafe { CGDisplayStreamStart(stream) } { 70 | CGError::Success => Ok(Capturer { 71 | stream, 72 | queue, 73 | width, 74 | height, 75 | format, 76 | display, 77 | stopped, 78 | }), 79 | x => Err(x), 80 | } 81 | } 82 | 83 | pub fn width(&self) -> usize { 84 | self.width 85 | } 86 | pub fn height(&self) -> usize { 87 | self.height 88 | } 89 | pub fn format(&self) -> PixelFormat { 90 | self.format 91 | } 92 | pub fn display(&self) -> Display { 93 | self.display 94 | } 95 | } 96 | 97 | impl Drop for Capturer { 98 | fn drop(&mut self) { 99 | unsafe { 100 | let _ = CGDisplayStreamStop(self.stream); 101 | loop { 102 | if *self.stopped.lock().unwrap() { 103 | break; 104 | } 105 | std::thread::sleep(std::time::Duration::from_millis(30)); 106 | } 107 | CFRelease(self.stream); 108 | dispatch_release(self.queue); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /libs/scrap/src/quartz/config.rs: -------------------------------------------------------------------------------- 1 | use std::ptr; 2 | 3 | use hbb_common::libc::c_void; 4 | 5 | use super::ffi::*; 6 | 7 | //TODO: Color space, YCbCr matrix. 8 | pub struct Config { 9 | /// Whether the cursor is visible. 10 | pub cursor: bool, 11 | /// Whether it should letterbox or stretch. 12 | pub letterbox: bool, 13 | /// Minimum seconds per frame. 14 | pub throttle: f64, 15 | /// How many frames are allocated. 16 | /// 3 is the recommended value. 17 | /// 8 is the maximum value. 18 | pub queue_length: i8, 19 | } 20 | 21 | impl Config { 22 | /// Don't forget to CFRelease this! 23 | pub fn build(self) -> CFDictionaryRef { 24 | unsafe { 25 | let throttle = CFNumberCreate( 26 | ptr::null_mut(), 27 | CFNumberType::Float64, 28 | &self.throttle as *const _ as *const c_void, 29 | ); 30 | let queue_length = CFNumberCreate( 31 | ptr::null_mut(), 32 | CFNumberType::SInt8, 33 | &self.queue_length as *const _ as *const c_void, 34 | ); 35 | 36 | let keys: [CFStringRef; 4] = [ 37 | kCGDisplayStreamShowCursor, 38 | kCGDisplayStreamPreserveAspectRatio, 39 | kCGDisplayStreamMinimumFrameTime, 40 | kCGDisplayStreamQueueDepth, 41 | ]; 42 | let values: [*mut c_void; 4] = [ 43 | cfbool(self.cursor), 44 | cfbool(self.letterbox), 45 | throttle, 46 | queue_length, 47 | ]; 48 | 49 | let res = CFDictionaryCreate( 50 | ptr::null_mut(), 51 | keys.as_ptr(), 52 | values.as_ptr(), 53 | 4, 54 | &kCFTypeDictionaryKeyCallBacks, 55 | &kCFTypeDictionaryValueCallBacks, 56 | ); 57 | 58 | CFRelease(throttle); 59 | CFRelease(queue_length); 60 | 61 | res 62 | } 63 | } 64 | } 65 | 66 | impl Default for Config { 67 | fn default() -> Config { 68 | Config { 69 | cursor: false, 70 | letterbox: true, 71 | throttle: 0.0, 72 | queue_length: 3, 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /libs/scrap/src/quartz/display.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use super::ffi::*; 4 | 5 | #[derive(PartialEq, Eq, Debug, Clone, Copy)] 6 | #[repr(C)] 7 | pub struct Display(u32); 8 | 9 | impl Display { 10 | pub fn primary() -> Display { 11 | Display(unsafe { CGMainDisplayID() }) 12 | } 13 | 14 | pub fn online() -> Result, CGError> { 15 | unsafe { 16 | #[allow(invalid_value)] 17 | let mut arr: [u32; 16] = mem::MaybeUninit::uninit().assume_init(); 18 | let mut len: u32 = 0; 19 | 20 | match CGGetOnlineDisplayList(16, arr.as_mut_ptr(), &mut len) { 21 | CGError::Success => (), 22 | x => return Err(x), 23 | } 24 | 25 | let mut res = Vec::with_capacity(16); 26 | for i in 0..len as usize { 27 | res.push(Display(*arr.get_unchecked(i))); 28 | } 29 | Ok(res) 30 | } 31 | } 32 | 33 | pub fn id(self) -> u32 { 34 | self.0 35 | } 36 | 37 | pub fn width(self) -> usize { 38 | let w = unsafe { CGDisplayPixelsWide(self.0) }; 39 | let s = self.scale(); 40 | if s > 1.0 { 41 | ((w as f64) * s).round() as usize 42 | } else { 43 | w 44 | } 45 | } 46 | 47 | pub fn height(self) -> usize { 48 | let h = unsafe { CGDisplayPixelsHigh(self.0) }; 49 | let s = self.scale(); 50 | if s > 1.0 { 51 | ((h as f64) * s).round() as usize 52 | } else { 53 | h 54 | } 55 | } 56 | 57 | pub fn is_builtin(self) -> bool { 58 | unsafe { CGDisplayIsBuiltin(self.0) != 0 } 59 | } 60 | 61 | pub fn is_primary(self) -> bool { 62 | unsafe { CGDisplayIsMain(self.0) != 0 } 63 | } 64 | 65 | pub fn is_active(self) -> bool { 66 | unsafe { CGDisplayIsActive(self.0) != 0 } 67 | } 68 | 69 | pub fn is_online(self) -> bool { 70 | unsafe { CGDisplayIsOnline(self.0) != 0 } 71 | } 72 | 73 | pub fn scale(self) -> f64 { 74 | let s = unsafe { BackingScaleFactor() as _ }; 75 | if s > 1. { 76 | let enable_retina = super::ENABLE_RETINA.lock().unwrap().clone(); 77 | if enable_retina { 78 | return s; 79 | } 80 | } 81 | 1. 82 | } 83 | 84 | pub fn bounds(self) -> CGRect { 85 | unsafe { CGDisplayBounds(self.0) } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /libs/scrap/src/quartz/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use block::RcBlock; 4 | use hbb_common::libc::c_void; 5 | 6 | pub type CGDisplayStreamRef = *mut c_void; 7 | pub type CFDictionaryRef = *mut c_void; 8 | pub type CFBooleanRef = *mut c_void; 9 | pub type CFNumberRef = *mut c_void; 10 | pub type CFStringRef = *mut c_void; 11 | pub type CGDisplayStreamUpdateRef = *mut c_void; 12 | pub type IOSurfaceRef = *mut c_void; 13 | pub type DispatchQueue = *mut c_void; 14 | pub type DispatchQueueAttr = *mut c_void; 15 | pub type CFAllocatorRef = *mut c_void; 16 | 17 | #[repr(C)] 18 | pub struct CFDictionaryKeyCallBacks { 19 | callbacks: [usize; 5], 20 | version: i32, 21 | } 22 | 23 | #[repr(C)] 24 | pub struct CFDictionaryValueCallBacks { 25 | callbacks: [usize; 4], 26 | version: i32, 27 | } 28 | 29 | macro_rules! pixel_format { 30 | ($a:expr, $b:expr, $c:expr, $d:expr) => { 31 | ($a as i32) << 24 | ($b as i32) << 16 | ($c as i32) << 8 | ($d as i32) 32 | }; 33 | } 34 | 35 | pub const SURFACE_LOCK_READ_ONLY: u32 = 0x0000_0001; 36 | pub const SURFACE_LOCK_AVOID_SYNC: u32 = 0x0000_0002; 37 | 38 | pub fn cfbool(x: bool) -> CFBooleanRef { 39 | unsafe { 40 | if x { 41 | kCFBooleanTrue 42 | } else { 43 | kCFBooleanFalse 44 | } 45 | } 46 | } 47 | 48 | #[repr(i32)] 49 | #[derive(PartialEq, Eq, Debug, Clone, Copy)] 50 | pub enum CGDisplayStreamFrameStatus { 51 | /// A new frame was generated. 52 | FrameComplete = 0, 53 | /// A new frame was not generated because the display did not change. 54 | FrameIdle = 1, 55 | /// A new frame was not generated because the display has gone blank. 56 | FrameBlank = 2, 57 | /// The display stream was stopped. 58 | Stopped = 3, 59 | #[doc(hidden)] 60 | __Nonexhaustive, 61 | } 62 | 63 | #[repr(i32)] 64 | #[derive(PartialEq, Eq, Debug, Clone, Copy)] 65 | pub enum CFNumberType { 66 | /* Fixed-width types */ 67 | SInt8 = 1, 68 | SInt16 = 2, 69 | SInt32 = 3, 70 | SInt64 = 4, 71 | Float32 = 5, 72 | Float64 = 6, 73 | /* 64-bit IEEE 754 */ 74 | /* Basic C types */ 75 | Char = 7, 76 | Short = 8, 77 | Int = 9, 78 | Long = 10, 79 | LongLong = 11, 80 | Float = 12, 81 | Double = 13, 82 | /* Other */ 83 | CFIndex = 14, 84 | NSInteger = 15, 85 | CGFloat = 16, 86 | } 87 | 88 | #[repr(i32)] 89 | #[derive(PartialEq, Eq, Debug, Clone, Copy)] 90 | #[must_use] 91 | pub enum CGError { 92 | Success = 0, 93 | Failure = 1000, 94 | IllegalArgument = 1001, 95 | InvalidConnection = 1002, 96 | InvalidContext = 1003, 97 | CannotComplete = 1004, 98 | NotImplemented = 1006, 99 | RangeCheck = 1007, 100 | TypeCheck = 1008, 101 | InvalidOperation = 1010, 102 | NoneAvailable = 1011, 103 | #[doc(hidden)] 104 | __Nonexhaustive, 105 | } 106 | 107 | #[repr(i32)] 108 | #[derive(PartialEq, Eq, Debug, Clone, Copy)] 109 | pub enum PixelFormat { 110 | /// Packed Little Endian ARGB8888 111 | Argb8888 = pixel_format!('B', 'G', 'R', 'A'), 112 | /// Packed Little Endian ARGB2101010 113 | Argb2101010 = pixel_format!('l', '1', '0', 'r'), 114 | /// 2-plane "video" range YCbCr 4:2:0 115 | YCbCr420Video = pixel_format!('4', '2', '0', 'v'), 116 | /// 2-plane "full" range YCbCr 4:2:0 117 | YCbCr420Full = pixel_format!('4', '2', '0', 'f'), 118 | #[doc(hidden)] 119 | __Nonexhaustive, 120 | } 121 | 122 | pub type CGDisplayStreamFrameAvailableHandler = *const c_void; 123 | 124 | pub type FrameAvailableHandler = RcBlock< 125 | ( 126 | CGDisplayStreamFrameStatus, // status 127 | u64, // displayTime 128 | IOSurfaceRef, // frameSurface 129 | CGDisplayStreamUpdateRef, // updateRef 130 | ), 131 | (), 132 | >; 133 | 134 | #[cfg(target_pointer_width = "64")] 135 | pub type CGFloat = f64; 136 | #[cfg(not(target_pointer_width = "64"))] 137 | pub type CGFloat = f32; 138 | #[repr(C)] 139 | pub struct CGPoint { 140 | pub x: CGFloat, 141 | pub y: CGFloat, 142 | } 143 | #[repr(C)] 144 | pub struct CGSize { 145 | pub width: CGFloat, 146 | pub height: CGFloat, 147 | } 148 | #[repr(C)] 149 | pub struct CGRect { 150 | pub origin: CGPoint, 151 | pub size: CGSize, 152 | } 153 | 154 | #[link(name = "System", kind = "dylib")] 155 | #[link(name = "CoreGraphics", kind = "framework")] 156 | #[link(name = "CoreFoundation", kind = "framework")] 157 | #[link(name = "IOSurface", kind = "framework")] 158 | extern "C" { 159 | // CoreGraphics 160 | 161 | pub static kCGDisplayStreamShowCursor: CFStringRef; 162 | pub static kCGDisplayStreamPreserveAspectRatio: CFStringRef; 163 | pub static kCGDisplayStreamMinimumFrameTime: CFStringRef; 164 | pub static kCGDisplayStreamQueueDepth: CFStringRef; 165 | 166 | pub fn CGDisplayStreamCreateWithDispatchQueue( 167 | display: u32, 168 | output_width: usize, 169 | output_height: usize, 170 | pixel_format: PixelFormat, 171 | properties: CFDictionaryRef, 172 | queue: DispatchQueue, 173 | handler: CGDisplayStreamFrameAvailableHandler, 174 | ) -> CGDisplayStreamRef; 175 | 176 | pub fn CGDisplayStreamStart(displayStream: CGDisplayStreamRef) -> CGError; 177 | 178 | pub fn CGDisplayStreamStop(displayStream: CGDisplayStreamRef) -> CGError; 179 | 180 | pub fn CGMainDisplayID() -> u32; 181 | pub fn CGDisplayPixelsWide(display: u32) -> usize; 182 | pub fn CGDisplayPixelsHigh(display: u32) -> usize; 183 | 184 | pub fn CGGetOnlineDisplayList( 185 | max_displays: u32, 186 | online_displays: *mut u32, 187 | display_count: *mut u32, 188 | ) -> CGError; 189 | 190 | pub fn CGDisplayIsBuiltin(display: u32) -> i32; 191 | pub fn CGDisplayIsMain(display: u32) -> i32; 192 | pub fn CGDisplayIsActive(display: u32) -> i32; 193 | pub fn CGDisplayIsOnline(display: u32) -> i32; 194 | 195 | pub fn CGDisplayBounds(display: u32) -> CGRect; 196 | pub fn BackingScaleFactor() -> f32; 197 | 198 | // IOSurface 199 | 200 | pub fn IOSurfaceGetAllocSize(buffer: IOSurfaceRef) -> usize; 201 | pub fn IOSurfaceGetBaseAddress(buffer: IOSurfaceRef) -> *mut c_void; 202 | pub fn IOSurfaceIncrementUseCount(buffer: IOSurfaceRef); 203 | pub fn IOSurfaceDecrementUseCount(buffer: IOSurfaceRef); 204 | pub fn IOSurfaceLock(buffer: IOSurfaceRef, options: u32, seed: *mut u32) -> i32; 205 | pub fn IOSurfaceUnlock(buffer: IOSurfaceRef, options: u32, seed: *mut u32) -> i32; 206 | pub fn IOSurfaceGetBaseAddressOfPlane(buffer: IOSurfaceRef, index: usize) -> *mut c_void; 207 | pub fn IOSurfaceGetBytesPerRowOfPlane(buffer: IOSurfaceRef, index: usize) -> usize; 208 | 209 | // Dispatch 210 | 211 | pub fn dispatch_queue_create(label: *const i8, attr: DispatchQueueAttr) -> DispatchQueue; 212 | 213 | pub fn dispatch_release(object: DispatchQueue); 214 | 215 | // Core Foundation 216 | 217 | pub static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks; 218 | pub static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks; 219 | 220 | // EVEN THE BOOLEANS ARE REFERENCES. 221 | pub static kCFBooleanTrue: CFBooleanRef; 222 | pub static kCFBooleanFalse: CFBooleanRef; 223 | 224 | pub fn CFNumberCreate( 225 | allocator: CFAllocatorRef, 226 | theType: CFNumberType, 227 | valuePtr: *const c_void, 228 | ) -> CFNumberRef; 229 | 230 | pub fn CFDictionaryCreate( 231 | allocator: CFAllocatorRef, 232 | keys: *const *mut c_void, 233 | values: *const *mut c_void, 234 | numValues: i64, 235 | keyCallBacks: *const CFDictionaryKeyCallBacks, 236 | valueCallBacks: *const CFDictionaryValueCallBacks, 237 | ) -> CFDictionaryRef; 238 | 239 | pub fn CFRetain(cf: *const c_void); 240 | pub fn CFRelease(cf: *const c_void); 241 | } 242 | -------------------------------------------------------------------------------- /libs/scrap/src/quartz/frame.rs: -------------------------------------------------------------------------------- 1 | use std::{ops, ptr, slice}; 2 | 3 | use super::ffi::*; 4 | 5 | pub struct Frame { 6 | surface: IOSurfaceRef, 7 | inner: &'static [u8], 8 | bgra: Vec, 9 | bgra_stride: usize, 10 | } 11 | 12 | impl Frame { 13 | pub unsafe fn new(surface: IOSurfaceRef) -> Frame { 14 | CFRetain(surface); 15 | IOSurfaceIncrementUseCount(surface); 16 | 17 | IOSurfaceLock(surface, SURFACE_LOCK_READ_ONLY, ptr::null_mut()); 18 | 19 | let inner = slice::from_raw_parts( 20 | IOSurfaceGetBaseAddress(surface) as *const u8, 21 | IOSurfaceGetAllocSize(surface), 22 | ); 23 | 24 | Frame { 25 | surface, 26 | inner, 27 | bgra: Vec::new(), 28 | bgra_stride: 0, 29 | } 30 | } 31 | 32 | #[inline] 33 | pub fn inner(&self) -> &[u8] { 34 | self.inner 35 | } 36 | 37 | pub fn stride(&self) -> usize { 38 | self.bgra_stride 39 | } 40 | 41 | pub fn surface_to_bgra<'a>(&'a mut self, h: usize) { 42 | unsafe { 43 | let plane0 = IOSurfaceGetBaseAddressOfPlane(self.surface, 0); 44 | self.bgra_stride = IOSurfaceGetBytesPerRowOfPlane(self.surface, 0); 45 | self.bgra.resize(self.bgra_stride * h, 0); 46 | std::ptr::copy_nonoverlapping( 47 | plane0 as _, 48 | self.bgra.as_mut_ptr(), 49 | self.bgra_stride * h, 50 | ); 51 | } 52 | } 53 | } 54 | 55 | impl ops::Deref for Frame { 56 | type Target = [u8]; 57 | fn deref<'a>(&'a self) -> &'a [u8] { 58 | &self.bgra 59 | } 60 | } 61 | 62 | impl Drop for Frame { 63 | fn drop(&mut self) { 64 | unsafe { 65 | IOSurfaceUnlock(self.surface, SURFACE_LOCK_READ_ONLY, ptr::null_mut()); 66 | 67 | IOSurfaceDecrementUseCount(self.surface); 68 | CFRelease(self.surface); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /libs/scrap/src/quartz/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::capturer::Capturer; 2 | pub use self::config::Config; 3 | pub use self::display::Display; 4 | pub use self::ffi::{CGError, PixelFormat}; 5 | pub use self::frame::Frame; 6 | 7 | mod capturer; 8 | mod config; 9 | mod display; 10 | pub mod ffi; 11 | mod frame; 12 | 13 | use std::sync::{Arc, Mutex}; 14 | 15 | lazy_static::lazy_static! { 16 | pub static ref ENABLE_RETINA: Arc> = Arc::new(Mutex::new(true)); 17 | } 18 | -------------------------------------------------------------------------------- /libs/scrap/src/wayland.rs: -------------------------------------------------------------------------------- 1 | pub mod capturable; 2 | pub mod pipewire; 3 | mod screencast_portal; 4 | mod request_portal; 5 | pub mod remote_desktop_portal; 6 | -------------------------------------------------------------------------------- /libs/scrap/src/wayland/README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | Derived from https://github.com/H-M-H/Weylus/tree/master/src/capturable with the author's consent, https://github.com/rustdesk/rustdesk/issues/56#issuecomment-882727967 4 | 5 | # Dep 6 | 7 | Works fine on Ubuntu 21.04 with pipewire 3 and xdg-desktop-portal 1.8 8 | 9 | ` 10 | apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev 11 | ` 12 | -------------------------------------------------------------------------------- /libs/scrap/src/wayland/capturable.rs: -------------------------------------------------------------------------------- 1 | use std::boxed::Box; 2 | use std::error::Error; 3 | 4 | pub enum PixelProvider<'a> { 5 | // 8 bits per color 6 | RGB(usize, usize, &'a [u8]), 7 | RGB0(usize, usize, &'a [u8]), 8 | BGR0(usize, usize, &'a [u8]), 9 | // width, height, stride 10 | BGR0S(usize, usize, usize, &'a [u8]), 11 | NONE, 12 | } 13 | 14 | impl<'a> PixelProvider<'a> { 15 | pub fn size(&self) -> (usize, usize) { 16 | match self { 17 | PixelProvider::RGB(w, h, _) => (*w, *h), 18 | PixelProvider::RGB0(w, h, _) => (*w, *h), 19 | PixelProvider::BGR0(w, h, _) => (*w, *h), 20 | PixelProvider::BGR0S(w, h, _, _) => (*w, *h), 21 | PixelProvider::NONE => (0, 0), 22 | } 23 | } 24 | } 25 | 26 | pub trait Recorder { 27 | fn capture(&mut self, timeout_ms: u64) -> Result>; 28 | } 29 | 30 | pub trait BoxCloneCapturable { 31 | fn box_clone(&self) -> Box; 32 | } 33 | 34 | impl BoxCloneCapturable for T 35 | where 36 | T: Clone + Capturable + 'static, 37 | { 38 | fn box_clone(&self) -> Box { 39 | Box::new(self.clone()) 40 | } 41 | } 42 | 43 | pub trait Capturable: Send + BoxCloneCapturable { 44 | /// Name of the Capturable, for example the window title, if it is a window. 45 | fn name(&self) -> String; 46 | /// Return x, y, width, height of the Capturable as floats relative to the absolute size of the 47 | /// screen. For example x=0.5, y=0.0, width=0.5, height=1.0 means the right half of the screen. 48 | fn geometry_relative(&self) -> Result<(f64, f64, f64, f64), Box>; 49 | /// Callback that is called right before input is simulated. 50 | /// Useful to focus the window on input. 51 | fn before_input(&mut self) -> Result<(), Box>; 52 | /// Return a Recorder that can record the current capturable. 53 | fn recorder(&self, capture_cursor: bool) -> Result, Box>; 54 | } 55 | 56 | impl Clone for Box { 57 | fn clone(&self) -> Self { 58 | self.box_clone() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /libs/scrap/src/wayland/request_portal.rs: -------------------------------------------------------------------------------- 1 | // This code was autogenerated with `dbus-codegen-rust -c blocking -m None`, see https://github.com/diwic/dbus-rs 2 | // https://github.com/flatpak/xdg-desktop-portal/blob/main/data/org.freedesktop.portal.Request.xml 3 | use dbus; 4 | #[allow(unused_imports)] 5 | use dbus::arg; 6 | use dbus::blocking; 7 | 8 | pub trait OrgFreedesktopPortalRequest { 9 | fn close(&self) -> Result<(), dbus::Error>; 10 | } 11 | 12 | impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> OrgFreedesktopPortalRequest 13 | for blocking::Proxy<'a, C> 14 | { 15 | fn close(&self) -> Result<(), dbus::Error> { 16 | self.method_call("org.freedesktop.portal.Request", "Close", ()) 17 | } 18 | } 19 | 20 | #[derive(Debug)] 21 | pub struct OrgFreedesktopPortalRequestResponse { 22 | pub response: u32, 23 | pub results: arg::PropMap, 24 | } 25 | 26 | impl arg::AppendAll for OrgFreedesktopPortalRequestResponse { 27 | fn append(&self, i: &mut arg::IterAppend) { 28 | arg::RefArg::append(&self.response, i); 29 | arg::RefArg::append(&self.results, i); 30 | } 31 | } 32 | 33 | impl arg::ReadAll for OrgFreedesktopPortalRequestResponse { 34 | fn read(i: &mut arg::Iter) -> Result { 35 | Ok(OrgFreedesktopPortalRequestResponse { 36 | response: i.read()?, 37 | results: i.read()?, 38 | }) 39 | } 40 | } 41 | 42 | impl dbus::message::SignalArgs for OrgFreedesktopPortalRequestResponse { 43 | const NAME: &'static str = "Response"; 44 | const INTERFACE: &'static str = "org.freedesktop.portal.Request"; 45 | } 46 | -------------------------------------------------------------------------------- /libs/scrap/src/wayland/screencast_portal.rs: -------------------------------------------------------------------------------- 1 | // This code was autogenerated with `dbus-codegen-rust -c blocking -m None`, see https://github.com/diwic/dbus-rs 2 | // https://github.com/flatpak/xdg-desktop-portal/blob/main/data/org.freedesktop.portal.ScreenCast.xml 3 | use dbus; 4 | #[allow(unused_imports)] 5 | use dbus::arg; 6 | use dbus::blocking; 7 | 8 | pub trait OrgFreedesktopPortalScreenCast { 9 | fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error>; 10 | fn select_sources( 11 | &self, 12 | session_handle: dbus::Path, 13 | options: arg::PropMap, 14 | ) -> Result, dbus::Error>; 15 | fn start( 16 | &self, 17 | session_handle: dbus::Path, 18 | parent_window: &str, 19 | options: arg::PropMap, 20 | ) -> Result, dbus::Error>; 21 | fn open_pipe_wire_remote( 22 | &self, 23 | session_handle: dbus::Path, 24 | options: arg::PropMap, 25 | ) -> Result; 26 | fn available_source_types(&self) -> Result; 27 | fn available_cursor_modes(&self) -> Result; 28 | fn version(&self) -> Result; 29 | } 30 | 31 | impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> 32 | OrgFreedesktopPortalScreenCast for blocking::Proxy<'a, C> 33 | { 34 | fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error> { 35 | self.method_call( 36 | "org.freedesktop.portal.ScreenCast", 37 | "CreateSession", 38 | (options,), 39 | ) 40 | .map(|r: (dbus::Path<'static>,)| r.0) 41 | } 42 | 43 | fn select_sources( 44 | &self, 45 | session_handle: dbus::Path, 46 | options: arg::PropMap, 47 | ) -> Result, dbus::Error> { 48 | self.method_call( 49 | "org.freedesktop.portal.ScreenCast", 50 | "SelectSources", 51 | (session_handle, options), 52 | ) 53 | .map(|r: (dbus::Path<'static>,)| r.0) 54 | } 55 | 56 | fn start( 57 | &self, 58 | session_handle: dbus::Path, 59 | parent_window: &str, 60 | options: arg::PropMap, 61 | ) -> Result, dbus::Error> { 62 | self.method_call( 63 | "org.freedesktop.portal.ScreenCast", 64 | "Start", 65 | (session_handle, parent_window, options), 66 | ) 67 | .map(|r: (dbus::Path<'static>,)| r.0) 68 | } 69 | 70 | fn open_pipe_wire_remote( 71 | &self, 72 | session_handle: dbus::Path, 73 | options: arg::PropMap, 74 | ) -> Result { 75 | self.method_call( 76 | "org.freedesktop.portal.ScreenCast", 77 | "OpenPipeWireRemote", 78 | (session_handle, options), 79 | ) 80 | .map(|r: (arg::OwnedFd,)| r.0) 81 | } 82 | 83 | fn available_source_types(&self) -> Result { 84 | ::get( 85 | &self, 86 | "org.freedesktop.portal.ScreenCast", 87 | "AvailableSourceTypes", 88 | ) 89 | } 90 | 91 | fn available_cursor_modes(&self) -> Result { 92 | ::get( 93 | &self, 94 | "org.freedesktop.portal.ScreenCast", 95 | "AvailableCursorModes", 96 | ) 97 | } 98 | 99 | fn version(&self) -> Result { 100 | ::get( 101 | &self, 102 | "org.freedesktop.portal.ScreenCast", 103 | "version", 104 | ) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /libs/scrap/src/x11/capturer.rs: -------------------------------------------------------------------------------- 1 | use super::ffi::*; 2 | use super::Display; 3 | use hbb_common::libc; 4 | use std::{io, ptr, slice}; 5 | 6 | pub struct Capturer { 7 | display: Display, 8 | shmid: i32, 9 | xcbid: u32, 10 | buffer: *const u8, 11 | 12 | size: usize, 13 | saved_raw_data: Vec, // for faster compare and copy 14 | } 15 | 16 | impl Capturer { 17 | pub fn new(display: Display) -> io::Result { 18 | // Calculate dimensions. 19 | 20 | let pixel_width = 4; 21 | let rect = display.rect(); 22 | let size = (rect.w as usize) * (rect.h as usize) * pixel_width; 23 | 24 | // Create a shared memory segment. 25 | 26 | let shmid = unsafe { 27 | libc::shmget( 28 | libc::IPC_PRIVATE, 29 | size, 30 | // Everyone can do anything. 31 | libc::IPC_CREAT | 0o777, 32 | ) 33 | }; 34 | 35 | if shmid == -1 { 36 | return Err(io::Error::last_os_error()); 37 | } 38 | 39 | // Attach the segment to a readable address. 40 | 41 | let buffer = unsafe { libc::shmat(shmid, ptr::null(), libc::SHM_RDONLY) } as *mut u8; 42 | 43 | if buffer as isize == -1 { 44 | return Err(io::Error::last_os_error()); 45 | } 46 | 47 | // Attach the segment to XCB. 48 | 49 | let server = display.server().raw(); 50 | let xcbid = unsafe { xcb_generate_id(server) }; 51 | unsafe { 52 | xcb_shm_attach( 53 | server, 54 | xcbid, 55 | shmid as u32, 56 | 0, // False, i.e. not read-only. 57 | ); 58 | } 59 | 60 | let c = Capturer { 61 | display, 62 | shmid, 63 | xcbid, 64 | buffer, 65 | size, 66 | saved_raw_data: Vec::new(), 67 | }; 68 | Ok(c) 69 | } 70 | 71 | pub fn display(&self) -> &Display { 72 | &self.display 73 | } 74 | 75 | fn get_image(&self) { 76 | let rect = self.display.rect(); 77 | unsafe { 78 | let request = xcb_shm_get_image_unchecked( 79 | self.display.server().raw(), 80 | self.display.root(), 81 | rect.x, 82 | rect.y, 83 | rect.w, 84 | rect.h, 85 | !0, 86 | XCB_IMAGE_FORMAT_Z_PIXMAP, 87 | self.xcbid, 88 | 0, 89 | ); 90 | let response = 91 | xcb_shm_get_image_reply(self.display.server().raw(), request, ptr::null_mut()); 92 | libc::free(response as *mut _); 93 | } 94 | } 95 | 96 | pub fn frame<'b>(&'b mut self) -> std::io::Result<&'b [u8]> { 97 | self.get_image(); 98 | let result = unsafe { slice::from_raw_parts(self.buffer, self.size) }; 99 | crate::would_block_if_equal(&mut self.saved_raw_data, result)?; 100 | Ok(result) 101 | } 102 | } 103 | 104 | impl Drop for Capturer { 105 | fn drop(&mut self) { 106 | unsafe { 107 | // Detach segment from XCB. 108 | xcb_shm_detach(self.display.server().raw(), self.xcbid); 109 | // Detach segment from our space. 110 | libc::shmdt(self.buffer as *mut _); 111 | // Destroy the shared memory segment. 112 | libc::shmctl(self.shmid, libc::IPC_RMID, ptr::null_mut()); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /libs/scrap/src/x11/display.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use super::ffi::*; 4 | use super::Server; 5 | 6 | #[derive(Debug)] 7 | pub struct Display { 8 | server: Rc, 9 | default: bool, 10 | rect: Rect, 11 | root: xcb_window_t, 12 | name: String, 13 | } 14 | 15 | #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] 16 | pub struct Rect { 17 | pub x: i16, 18 | pub y: i16, 19 | pub w: u16, 20 | pub h: u16, 21 | } 22 | 23 | impl Display { 24 | pub unsafe fn new( 25 | server: Rc, 26 | default: bool, 27 | rect: Rect, 28 | root: xcb_window_t, 29 | name: String, 30 | ) -> Display { 31 | Display { 32 | server, 33 | default, 34 | rect, 35 | root, 36 | name, 37 | } 38 | } 39 | 40 | pub fn server(&self) -> &Rc { 41 | &self.server 42 | } 43 | pub fn is_default(&self) -> bool { 44 | self.default 45 | } 46 | pub fn rect(&self) -> Rect { 47 | self.rect 48 | } 49 | pub fn w(&self) -> usize { 50 | self.rect.w as _ 51 | } 52 | pub fn h(&self) -> usize { 53 | self.rect.h as _ 54 | } 55 | pub fn root(&self) -> xcb_window_t { 56 | self.root 57 | } 58 | 59 | pub fn name(&self) -> String { 60 | self.name.clone() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /libs/scrap/src/x11/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | 3 | use hbb_common::libc::c_void; 4 | 5 | #[link(name = "xcb")] 6 | #[link(name = "xcb-shm")] 7 | #[link(name = "xcb-randr")] 8 | extern "C" { 9 | pub fn xcb_connect(displayname: *const i8, screenp: *mut i32) -> *mut xcb_connection_t; 10 | 11 | pub fn xcb_disconnect(c: *mut xcb_connection_t); 12 | 13 | pub fn xcb_connection_has_error(c: *mut xcb_connection_t) -> i32; 14 | 15 | pub fn xcb_get_setup(c: *mut xcb_connection_t) -> *const xcb_setup_t; 16 | 17 | pub fn xcb_setup_roots_iterator(r: *const xcb_setup_t) -> xcb_screen_iterator_t; 18 | 19 | pub fn xcb_screen_next(i: *mut xcb_screen_iterator_t); 20 | 21 | pub fn xcb_generate_id(c: *mut xcb_connection_t) -> u32; 22 | 23 | pub fn xcb_shm_attach( 24 | c: *mut xcb_connection_t, 25 | shmseg: xcb_shm_seg_t, 26 | shmid: u32, 27 | read_only: u8, 28 | ) -> xcb_void_cookie_t; 29 | 30 | pub fn xcb_shm_detach(c: *mut xcb_connection_t, shmseg: xcb_shm_seg_t) -> xcb_void_cookie_t; 31 | 32 | pub fn xcb_shm_get_image_unchecked( 33 | c: *mut xcb_connection_t, 34 | drawable: xcb_drawable_t, 35 | x: i16, 36 | y: i16, 37 | width: u16, 38 | height: u16, 39 | plane_mask: u32, 40 | format: u8, 41 | shmseg: xcb_shm_seg_t, 42 | offset: u32, 43 | ) -> xcb_shm_get_image_cookie_t; 44 | 45 | pub fn xcb_shm_get_image_reply( 46 | c: *mut xcb_connection_t, 47 | cookie: xcb_shm_get_image_cookie_t, 48 | e: *mut *mut xcb_generic_error_t, 49 | ) -> *mut xcb_shm_get_image_reply_t; 50 | 51 | pub fn xcb_randr_get_monitors_unchecked( 52 | c: *mut xcb_connection_t, 53 | window: xcb_window_t, 54 | get_active: u8, 55 | ) -> xcb_randr_get_monitors_cookie_t; 56 | 57 | pub fn xcb_randr_get_monitors_reply( 58 | c: *mut xcb_connection_t, 59 | cookie: xcb_randr_get_monitors_cookie_t, 60 | e: *mut *mut xcb_generic_error_t, 61 | ) -> *mut xcb_randr_get_monitors_reply_t; 62 | 63 | pub fn xcb_randr_get_monitors_monitors_iterator( 64 | r: *const xcb_randr_get_monitors_reply_t, 65 | ) -> xcb_randr_monitor_info_iterator_t; 66 | 67 | pub fn xcb_randr_monitor_info_next(i: *mut xcb_randr_monitor_info_iterator_t); 68 | 69 | pub fn xcb_get_atom_name( 70 | c: *mut xcb_connection_t, 71 | atom: xcb_atom_t, 72 | ) -> xcb_get_atom_name_cookie_t; 73 | 74 | pub fn xcb_get_atom_name_reply( 75 | c: *mut xcb_connection_t, 76 | cookie: xcb_get_atom_name_cookie_t, 77 | e: *mut *mut xcb_generic_error_t, 78 | ) -> *const xcb_get_atom_name_reply_t; 79 | 80 | pub fn xcb_get_atom_name_name(reply: *const xcb_get_atom_name_request_t) -> *const u8; 81 | 82 | pub fn xcb_get_atom_name_name_length(reply: *const xcb_get_atom_name_reply_t) -> i32; 83 | 84 | pub fn xcb_shm_query_version(c: *mut xcb_connection_t) -> xcb_shm_query_version_cookie_t; 85 | 86 | pub fn xcb_shm_query_version_reply( 87 | c: *mut xcb_connection_t, 88 | cookie: xcb_shm_query_version_cookie_t, 89 | e: *mut *mut xcb_generic_error_t, 90 | ) -> *const xcb_shm_query_version_reply_t; 91 | } 92 | 93 | pub const XCB_IMAGE_FORMAT_Z_PIXMAP: u8 = 2; 94 | 95 | pub type xcb_atom_t = u32; 96 | pub type xcb_connection_t = c_void; 97 | pub type xcb_window_t = u32; 98 | pub type xcb_keycode_t = u8; 99 | pub type xcb_visualid_t = u32; 100 | pub type xcb_timestamp_t = u32; 101 | pub type xcb_colormap_t = u32; 102 | pub type xcb_shm_seg_t = u32; 103 | pub type xcb_drawable_t = u32; 104 | pub type xcb_get_atom_name_cookie_t = u32; 105 | pub type xcb_get_atom_name_reply_t = u32; 106 | pub type xcb_get_atom_name_request_t = xcb_get_atom_name_reply_t; 107 | 108 | #[repr(C)] 109 | pub struct xcb_setup_t { 110 | pub status: u8, 111 | pub pad0: u8, 112 | pub protocol_major_version: u16, 113 | pub protocol_minor_version: u16, 114 | pub length: u16, 115 | pub release_number: u32, 116 | pub resource_id_base: u32, 117 | pub resource_id_mask: u32, 118 | pub motion_buffer_size: u32, 119 | pub vendor_len: u16, 120 | pub maximum_request_length: u16, 121 | pub roots_len: u8, 122 | pub pixmap_formats_len: u8, 123 | pub image_byte_order: u8, 124 | pub bitmap_format_bit_order: u8, 125 | pub bitmap_format_scanline_unit: u8, 126 | pub bitmap_format_scanline_pad: u8, 127 | pub min_keycode: xcb_keycode_t, 128 | pub max_keycode: xcb_keycode_t, 129 | pub pad1: [u8; 4], 130 | } 131 | 132 | #[repr(C)] 133 | pub struct xcb_screen_iterator_t { 134 | pub data: *mut xcb_screen_t, 135 | pub rem: i32, 136 | pub index: i32, 137 | } 138 | 139 | #[repr(C)] 140 | pub struct xcb_screen_t { 141 | pub root: xcb_window_t, 142 | pub default_colormap: xcb_colormap_t, 143 | pub white_pixel: u32, 144 | pub black_pixel: u32, 145 | pub current_input_masks: u32, 146 | pub width_in_pixels: u16, 147 | pub height_in_pixels: u16, 148 | pub width_in_millimeters: u16, 149 | pub height_in_millimeters: u16, 150 | pub min_installed_maps: u16, 151 | pub max_installed_maps: u16, 152 | pub root_visual: xcb_visualid_t, 153 | pub backing_stores: u8, 154 | pub save_unders: u8, 155 | pub root_depth: u8, 156 | pub allowed_depths_len: u8, 157 | } 158 | 159 | #[repr(C)] 160 | pub struct xcb_randr_monitor_info_iterator_t { 161 | pub data: *mut xcb_randr_monitor_info_t, 162 | pub rem: i32, 163 | pub index: i32, 164 | } 165 | 166 | #[repr(C)] 167 | pub struct xcb_randr_monitor_info_t { 168 | pub name: xcb_atom_t, 169 | pub primary: u8, 170 | pub automatic: u8, 171 | pub n_output: u16, 172 | pub x: i16, 173 | pub y: i16, 174 | pub width: u16, 175 | pub height: u16, 176 | pub width_mm: u32, 177 | pub height_mm: u32, 178 | } 179 | 180 | #[repr(C)] 181 | #[derive(Clone, Copy)] 182 | pub struct xcb_randr_get_monitors_cookie_t { 183 | pub sequence: u32, 184 | } 185 | 186 | #[repr(C)] 187 | #[derive(Clone, Copy)] 188 | pub struct xcb_shm_get_image_cookie_t { 189 | pub sequence: u32, 190 | } 191 | 192 | #[repr(C)] 193 | #[derive(Clone, Copy)] 194 | pub struct xcb_void_cookie_t { 195 | pub sequence: u32, 196 | } 197 | 198 | #[repr(C)] 199 | pub struct xcb_generic_error_t { 200 | pub response_type: u8, 201 | pub error_code: u8, 202 | pub sequence: u16, 203 | pub resource_id: u32, 204 | pub minor_code: u16, 205 | pub major_code: u8, 206 | pub pad0: u8, 207 | pub pad: [u32; 5], 208 | pub full_sequence: u32, 209 | } 210 | 211 | #[repr(C)] 212 | pub struct xcb_shm_get_image_reply_t { 213 | pub response_type: u8, 214 | pub depth: u8, 215 | pub sequence: u16, 216 | pub length: u32, 217 | pub visual: xcb_visualid_t, 218 | pub size: u32, 219 | } 220 | 221 | #[repr(C)] 222 | pub struct xcb_randr_get_monitors_reply_t { 223 | pub response_type: u8, 224 | pub pad0: u8, 225 | pub sequence: u16, 226 | pub length: u32, 227 | pub timestamp: xcb_timestamp_t, 228 | pub n_monitors: u32, 229 | pub n_outputs: u32, 230 | pub pad1: [u8; 12], 231 | } 232 | 233 | #[repr(C)] 234 | pub struct xcb_shm_query_version_cookie_t { 235 | pub sequence: u32, 236 | } 237 | 238 | #[repr(C)] 239 | pub struct xcb_shm_query_version_reply_t { 240 | pub response_type: u8, 241 | pub shared_pixmaps: u8, 242 | pub sequence: u16, 243 | pub length: u32, 244 | pub major_version: u16, 245 | pub minor_version: u16, 246 | pub uid: u16, 247 | pub gid: u16, 248 | pub pixmap_format: u8, 249 | pub pad0: [u8; 15], 250 | } 251 | -------------------------------------------------------------------------------- /libs/scrap/src/x11/iter.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | use std::ptr; 3 | use std::rc::Rc; 4 | 5 | use hbb_common::libc; 6 | 7 | use super::ffi::*; 8 | use super::{Display, Rect, Server}; 9 | 10 | //TODO: Do I have to free the displays? 11 | 12 | pub struct DisplayIter { 13 | outer: xcb_screen_iterator_t, 14 | inner: Option<(xcb_randr_monitor_info_iterator_t, xcb_window_t)>, 15 | server: Rc, 16 | } 17 | 18 | impl DisplayIter { 19 | pub unsafe fn new(server: Rc) -> DisplayIter { 20 | let mut outer = xcb_setup_roots_iterator(server.setup()); 21 | let inner = Self::next_screen(&mut outer, &server); 22 | DisplayIter { 23 | outer, 24 | inner, 25 | server, 26 | } 27 | } 28 | 29 | fn next_screen( 30 | outer: &mut xcb_screen_iterator_t, 31 | server: &Server, 32 | ) -> Option<(xcb_randr_monitor_info_iterator_t, xcb_window_t)> { 33 | if outer.rem == 0 { 34 | return None; 35 | } 36 | 37 | unsafe { 38 | let root = (*outer.data).root; 39 | 40 | let cookie = xcb_randr_get_monitors_unchecked( 41 | server.raw(), 42 | root, 43 | 1, //TODO: I don't know if this should be true or false. 44 | ); 45 | 46 | let response = xcb_randr_get_monitors_reply(server.raw(), cookie, ptr::null_mut()); 47 | 48 | let inner = xcb_randr_get_monitors_monitors_iterator(response); 49 | 50 | libc::free(response as *mut _); 51 | xcb_screen_next(outer); 52 | 53 | Some((inner, root)) 54 | } 55 | } 56 | } 57 | 58 | impl Iterator for DisplayIter { 59 | type Item = Display; 60 | 61 | fn next(&mut self) -> Option { 62 | loop { 63 | if let Some((ref mut inner, root)) = self.inner { 64 | // If there is something in the current screen, return that. 65 | if inner.rem != 0 { 66 | unsafe { 67 | let data = &*inner.data; 68 | let name = get_atom_name(self.server.raw(), data.name); 69 | 70 | let display = Display::new( 71 | self.server.clone(), 72 | data.primary != 0, 73 | Rect { 74 | x: data.x, 75 | y: data.y, 76 | w: data.width, 77 | h: data.height, 78 | }, 79 | root, 80 | name, 81 | ); 82 | 83 | xcb_randr_monitor_info_next(inner); 84 | return Some(display); 85 | } 86 | } 87 | } else { 88 | // If there is no current screen, the screen iterator is empty. 89 | return None; 90 | } 91 | 92 | // The current screen was empty, so try the next screen. 93 | self.inner = Self::next_screen(&mut self.outer, &self.server); 94 | } 95 | } 96 | } 97 | 98 | fn get_atom_name(conn: *mut xcb_connection_t, atom: xcb_atom_t) -> String { 99 | let empty = "".to_owned(); 100 | if atom == 0 { 101 | return empty; 102 | } 103 | unsafe { 104 | let mut e: *mut xcb_generic_error_t = std::ptr::null_mut(); 105 | let reply = xcb_get_atom_name_reply( 106 | conn, 107 | xcb_get_atom_name(conn, atom), 108 | &mut e as _, 109 | ); 110 | if reply == std::ptr::null() { 111 | return empty; 112 | } 113 | let length = xcb_get_atom_name_name_length(reply); 114 | let name = xcb_get_atom_name_name(reply); 115 | let mut v = vec![0u8; length as _]; 116 | std::ptr::copy_nonoverlapping(name as _, v.as_mut_ptr(), length as _); 117 | libc::free(reply as *mut _); 118 | if let Ok(s) = CString::new(v) { 119 | return s.to_string_lossy().to_string(); 120 | } 121 | empty 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /libs/scrap/src/x11/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::capturer::*; 2 | pub use self::display::*; 3 | pub use self::iter::*; 4 | pub use self::server::*; 5 | 6 | mod capturer; 7 | mod display; 8 | mod ffi; 9 | mod iter; 10 | mod server; 11 | -------------------------------------------------------------------------------- /libs/scrap/src/x11/server.rs: -------------------------------------------------------------------------------- 1 | use std::ptr; 2 | use std::rc::Rc; 3 | 4 | use super::ffi::*; 5 | use super::DisplayIter; 6 | 7 | #[derive(Debug)] 8 | pub struct Server { 9 | raw: *mut xcb_connection_t, 10 | screenp: i32, 11 | setup: *const xcb_setup_t, 12 | } 13 | 14 | /* 15 | use std::cell::RefCell; 16 | thread_local! { 17 | static SERVER: RefCell>> = RefCell::new(None); 18 | } 19 | */ 20 | 21 | impl Server { 22 | pub fn displays(slf: Rc) -> DisplayIter { 23 | unsafe { DisplayIter::new(slf) } 24 | } 25 | 26 | pub fn default() -> Result, Error> { 27 | Ok(Rc::new(Server::connect(ptr::null())?)) 28 | /* 29 | let mut res = Err(Error::from(0)); 30 | SERVER.with(|xdo| { 31 | if let Ok(mut server) = xdo.try_borrow_mut() { 32 | if server.is_some() { 33 | unsafe { 34 | if 0 != xcb_connection_has_error(server.as_ref().unwrap().raw) { 35 | *server = None; 36 | println!("Reset x11 connection"); 37 | } 38 | } 39 | } 40 | if server.is_none() { 41 | println!("New x11 connection"); 42 | match Server::connect(ptr::null()) { 43 | Ok(s) => { 44 | let s = Rc::new(s); 45 | res = Ok(s.clone()); 46 | *server = Some(s); 47 | } 48 | Err(err) => { 49 | res = Err(err); 50 | } 51 | } 52 | } else { 53 | res = Ok(server.as_ref().map(|x| x.clone()).unwrap()); 54 | } 55 | } 56 | }); 57 | res 58 | */ 59 | } 60 | 61 | pub fn connect(addr: *const i8) -> Result { 62 | unsafe { 63 | let mut screenp = 0; 64 | let raw = xcb_connect(addr, &mut screenp); 65 | 66 | let error = xcb_connection_has_error(raw); 67 | if error != 0 { 68 | xcb_disconnect(raw); 69 | Err(Error::from(error)) 70 | } else { 71 | let setup = xcb_get_setup(raw); 72 | Ok(Server { 73 | raw, 74 | screenp, 75 | setup, 76 | }) 77 | } 78 | } 79 | } 80 | 81 | pub fn raw(&self) -> *mut xcb_connection_t { 82 | self.raw 83 | } 84 | pub fn screenp(&self) -> i32 { 85 | self.screenp 86 | } 87 | pub fn setup(&self) -> *const xcb_setup_t { 88 | self.setup 89 | } 90 | pub fn get_shm_status(&self) -> Result<(), Error> { 91 | unsafe { check_x11_shm_available(self.raw) } 92 | } 93 | } 94 | 95 | unsafe fn check_x11_shm_available(c: *mut xcb_connection_t) -> Result<(), Error> { 96 | let cookie = xcb_shm_query_version(c); 97 | let mut e: *mut xcb_generic_error_t = std::ptr::null_mut(); 98 | let reply = xcb_shm_query_version_reply(c, cookie, &mut e as _); 99 | if reply.is_null() { 100 | // TODO: Should seperate SHM disabled from SHM not supported? 101 | return Err(Error::UnsupportedExtension); 102 | } else if e.is_null() { 103 | return Ok(()); 104 | } else { 105 | // TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here? 106 | return Err(Error::Generic); 107 | } 108 | } 109 | 110 | impl Drop for Server { 111 | fn drop(&mut self) { 112 | unsafe { 113 | xcb_disconnect(self.raw); 114 | } 115 | } 116 | } 117 | 118 | #[derive(Clone, Copy, Debug)] 119 | pub enum Error { 120 | Generic, 121 | UnsupportedExtension, 122 | InsufficientMemory, 123 | RequestTooLong, 124 | ParseError, 125 | InvalidScreen, 126 | } 127 | 128 | impl From for Error { 129 | fn from(x: i32) -> Error { 130 | use self::Error::*; 131 | match x { 132 | 2 => UnsupportedExtension, 133 | 3 => InsufficientMemory, 134 | 4 => RequestTooLong, 135 | 5 => ParseError, 136 | 6 => InvalidScreen, 137 | _ => Generic, 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recordscript", 3 | "version": "0.0.0", 4 | "description": "Recordscript lets you generate subtitle either from built-in screen recorder or from video and audio files", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite", 8 | "dev": "vite", 9 | "build": "vite build", 10 | "serve": "vite preview", 11 | "tauri": "tauri" 12 | }, 13 | "license": "GPL-3.0-only", 14 | "dependencies": { 15 | "@ffmpeg/ffmpeg": "^0.12.10", 16 | "@ffmpeg/util": "^0.12.1", 17 | "@solid-primitives/map": "^0.4.11", 18 | "@tauri-apps/api": "^1.5.2", 19 | "solid-js": "^1.7.8" 20 | }, 21 | "devDependencies": { 22 | "@tauri-apps/cli": "^1.5.8", 23 | "autoprefixer": "^10.4.17", 24 | "postcss": "^8.4.35", 25 | "tailwindcss": "^3.4.1", 26 | "typescript": "^5.0.2", 27 | "vite": "^5.0.0", 28 | "vite-plugin-solid": "^2.8.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /record-control.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Recordscript 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "recordscript" 3 | version = "0.0.0" 4 | description = "Recordscript" 5 | authors = ["you"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [build-dependencies] 11 | tauri-build = { version = "1.5", features = [] } 12 | 13 | [dependencies] 14 | anyhow = "1.0.80" 15 | cpal = "0.15.2" 16 | directories = "5.0.1" 17 | tauri = { version = "1.5", features = ["updater", "api-all", "dialog"] } 18 | reqwest = "0.11.24" 19 | serde = { version = "1.0", features = ["derive"] } 20 | serde_json = "1.0" 21 | strum = "0.26" 22 | strum_macros = "0.26" 23 | whisper-rs = { git = "https://github.com/tazz4843/whisper-rs", rev = "f1030ef" } 24 | scrap = { path = "../libs/scrap" } 25 | tokio = { version = "1.37.0", features = ["full"] } 26 | gst = { version = "0.22.5", package = "gstreamer", features = ["v1_24"] } 27 | gst_app = { version = "0.22.0", package = "gstreamer-app", features = [ 28 | "v1_24", 29 | ] } 30 | gst-plugin-fallbackswitch = "0.12.4" 31 | byte-slice-cast = "1.2.2" 32 | dialog = "0.3.0" 33 | bincode = "1.3.3" 34 | keyring = "2.3.3" 35 | chrono = "0.4.38" 36 | lettre = "0.11.7" 37 | showfile = "0.1.1" 38 | bytemuck = "1.16.1" 39 | sysinfo = "0.30.12" 40 | 41 | [features] 42 | # this feature is used for production builds or when `devPath` points to the filesystem 43 | # DO NOT REMOVE!! 44 | custom-protocol = ["tauri/custom-protocol"] 45 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/configuration.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, time::Duration}; 2 | 3 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 4 | 5 | fn config_path() -> PathBuf { 6 | let config_path = super::project_directory().config_dir().to_path_buf(); 7 | 8 | std::fs::create_dir_all(&config_path).expect("Can't create config directory"); 9 | 10 | config_path.join(format!("{}.bin", std::any::type_name::()).replace("::", "-")) 11 | } 12 | 13 | fn document_path() -> PathBuf { 14 | let user_dirs = directories::UserDirs::new().expect("Can't find user directory"); 15 | 16 | let path = user_dirs 17 | .document_dir() 18 | .unwrap_or_else(|| { 19 | eprintln!("Can't find user document directory, falling back to home directory"); 20 | 21 | user_dirs.home_dir() 22 | }) 23 | .join("Recordscript"); 24 | 25 | std::fs::create_dir_all(&path).expect("Can't create transcription directory"); 26 | 27 | path 28 | } 29 | 30 | pub fn save(data: &D) 31 | where 32 | D: Serialize, 33 | { 34 | let data = bincode::serialize(data).expect("Can't serialize config"); 35 | 36 | std::fs::write(config_path::(), data).expect("Can't save app configuration"); 37 | } 38 | 39 | fn load() -> anyhow::Result 40 | where 41 | D: DeserializeOwned, 42 | { 43 | let data = std::fs::read(config_path::())?; 44 | let data: D = bincode::deserialize(&data)?; 45 | 46 | Ok(data) 47 | } 48 | 49 | #[derive(Debug, Clone, Serialize, Deserialize)] 50 | pub struct SavePathConfig { 51 | pub save_path: PathBuf, 52 | pub save_path_histories: Vec, 53 | } 54 | 55 | impl Default for SavePathConfig { 56 | fn default() -> Self { 57 | Self { 58 | save_path: document_path(), 59 | save_path_histories: vec![document_path()], 60 | } 61 | } 62 | } 63 | 64 | #[derive(Debug, Clone, Serialize, Deserialize)] 65 | pub struct GeneralConfig { 66 | pub transcript: bool, 67 | pub translate: bool, 68 | pub transcription_email_to: String, 69 | pub save_to: SavePathConfig, 70 | pub transcript_save_to: SavePathConfig, 71 | } 72 | 73 | impl Default for GeneralConfig { 74 | fn default() -> Self { 75 | if let Ok(this) = load::() { 76 | return this; 77 | } 78 | 79 | let this = Self { 80 | transcript: false, 81 | translate: false, 82 | transcription_email_to: String::new(), 83 | save_to: SavePathConfig::default(), 84 | transcript_save_to: SavePathConfig::default(), 85 | }; 86 | 87 | save(&this); 88 | 89 | this 90 | } 91 | } 92 | 93 | #[derive(Debug, Clone, Serialize, Deserialize)] 94 | pub struct SMTPConfig { 95 | pub host: String, 96 | pub port: u16, 97 | pub username: String, 98 | pub password: String, 99 | pub from: String, 100 | } 101 | 102 | impl Default for SMTPConfig { 103 | fn default() -> Self { 104 | if let Ok(this) = load::() { 105 | return this; 106 | } 107 | 108 | let this = Self { 109 | host: String::new(), 110 | port: 465, 111 | username: String::new(), 112 | password: String::new(), 113 | from: String::new(), 114 | }; 115 | 116 | save(&this); 117 | 118 | this 119 | } 120 | } 121 | 122 | impl SMTPConfig { 123 | pub fn auto_smtp_transport(&self) -> anyhow::Result { 124 | use lettre::SmtpTransport; 125 | 126 | let creds = lettre::transport::smtp::authentication::Credentials::new( 127 | self.username.clone(), 128 | self.password.clone(), 129 | ); 130 | 131 | let tls = || { 132 | println!("Trying to establish a TLS connection"); 133 | 134 | let transport = SmtpTransport::relay(&self.host)? 135 | .port(self.port) 136 | .credentials(creds.clone()) 137 | .timeout(Some(Duration::from_secs(10))) 138 | .build(); 139 | 140 | match transport.test_connection() { 141 | Ok(false) => anyhow::bail!("Couldn't connect to SMTP server with TLS"), 142 | Err(e) => anyhow::bail!("Couldn't connect to SMTP server with TLS because:\n{e}"), 143 | _ => {} 144 | } 145 | 146 | Ok(transport) 147 | }; 148 | 149 | let tls_over_plaintext = || { 150 | println!("Trying to establish a TLS connection over plaintext"); 151 | 152 | let transport = SmtpTransport::starttls_relay(&self.host)? 153 | .port(self.port) 154 | .credentials(creds.clone()) 155 | .timeout(Some(Duration::from_secs(10))) 156 | .build(); 157 | 158 | match transport.test_connection() { 159 | Ok(false) => { 160 | anyhow::bail!("Couldn't connect to SMTP server with TLS over plaintext") 161 | } 162 | Err(e) => anyhow::bail!( 163 | "Couldn't connect to SMTP server with TLS over plaintext because:\n{e}" 164 | ), 165 | _ => {} 166 | } 167 | 168 | Ok(transport) 169 | }; 170 | 171 | let plaintext = || { 172 | println!("Trying to establish a plaintext connection"); 173 | 174 | let transport = SmtpTransport::builder_dangerous(&self.host) 175 | .port(self.port) 176 | .credentials(creds.clone()) 177 | .timeout(Some(Duration::from_secs(10))) 178 | .build(); 179 | 180 | match transport.test_connection() { 181 | Ok(false) => anyhow::bail!("Couldn't connect to SMTP server with plaintext"), 182 | Err(e) => { 183 | anyhow::bail!("Couldn't connect to SMTP server with plaintext because:\n{e}") 184 | } 185 | _ => {} 186 | } 187 | 188 | Ok(transport) 189 | }; 190 | 191 | tls() 192 | .map_or_else(|_| tls_over_plaintext(), Ok) 193 | .map_or_else(|_| plaintext(), Ok) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src-tauri/src/recorder.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use cpal::traits::DeviceTrait as _; 4 | use cpal::traits::HostTrait as _; 5 | use cpal::{Device, Host}; 6 | 7 | use scrap::Display; 8 | use scrap::TraitCapturer as _; 9 | use scrap::TraitPixelBuffer as _; 10 | 11 | use strum_macros::{EnumIter, IntoStaticStr}; 12 | 13 | #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, EnumIter, IntoStaticStr)] 14 | pub enum DeviceType { 15 | Microphone, 16 | Speaker, 17 | } 18 | 19 | pub struct SelectedDevice { 20 | pub microphone: Option, 21 | pub speaker: Option, 22 | pub screen: Display, 23 | } 24 | 25 | unsafe impl Send for SelectedDevice { } 26 | 27 | impl Clone for SelectedDevice { 28 | fn clone(&self) -> Self { 29 | Self { 30 | microphone: self.microphone.clone(), 31 | speaker: self.speaker.clone(), 32 | screen: self.screen.clone_device(), 33 | } 34 | } 35 | } 36 | 37 | pub trait DeviceEq { 38 | fn eq_device(&self, device: &Self) -> bool; 39 | } 40 | 41 | impl DeviceEq for Device { 42 | fn eq_device(&self, device: &Device) -> bool { 43 | let a = self.name().unwrap_or_default(); 44 | let b = device.name().unwrap_or_default(); 45 | 46 | a.eq(&b) 47 | } 48 | } 49 | 50 | impl DeviceEq for Display { 51 | fn eq_device(&self, device: &Display) -> bool { 52 | let a = self.name(); 53 | let b = device.name(); 54 | 55 | a.eq(&b) 56 | } 57 | } 58 | 59 | pub trait DeviceClone { 60 | fn clone_device(&self) -> Self; 61 | } 62 | 63 | impl DeviceClone for Display { 64 | fn clone_device(&self) -> Self { 65 | list_screen().unwrap().into_iter() 66 | .filter(|d| d.eq_device(self)) 67 | .nth(0).unwrap() 68 | } 69 | } 70 | 71 | pub enum RecordCommand { 72 | Start(SelectedDevice), 73 | Pause, 74 | Resume, 75 | Stop 76 | } 77 | 78 | #[derive(serde::Serialize)] 79 | pub struct DeviceResult { 80 | pub name: String, 81 | pub is_selected: bool, 82 | } 83 | 84 | fn all_hosts() -> Vec { 85 | cpal::ALL_HOSTS.iter() 86 | .map(|host_id| cpal::host_from_id(*host_id)) 87 | .filter_map(|host| host.ok()) 88 | .collect() 89 | } 90 | 91 | pub fn list_microphone() -> Vec { 92 | all_hosts().into_iter() 93 | .map(|host| host.input_devices()) 94 | .filter_map(|devices| devices.ok()) 95 | .flat_map(|devices| devices.collect::>()) 96 | .collect() 97 | } 98 | 99 | pub fn list_speaker() -> Vec { 100 | all_hosts().into_iter() 101 | .map(|host| host.output_devices()) 102 | .filter_map(|devices| devices.ok()) 103 | .flat_map(|devices| devices.collect::>()) 104 | .collect() 105 | } 106 | 107 | pub fn list_screen() -> anyhow::Result> { 108 | Ok(scrap::Display::all()?) 109 | } 110 | 111 | pub struct Screen { 112 | name: String, 113 | display: Display, 114 | } 115 | 116 | impl Screen { 117 | pub fn name(&self) -> &String { 118 | &self.name 119 | } 120 | 121 | /// Return PNG encoded screen preview 122 | pub fn preview(&self) -> anyhow::Result> { 123 | use gst::prelude::*; 124 | use scrap::Capturer; 125 | 126 | let mut capturer = Capturer::new(self.display.clone_device()).unwrap(); 127 | 128 | let width; 129 | let height; 130 | let data; 131 | 132 | loop { 133 | let frame = match capturer.frame(std::time::Duration::ZERO) { 134 | Ok(frame) => frame, 135 | Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => continue, 136 | Err(err) if err.kind() == std::io::ErrorKind::InvalidData => { 137 | eprintln!("Received invalid data, skipping"); 138 | continue 139 | }, 140 | Err(err) => panic!("{err}"), 141 | }; 142 | 143 | let scrap::Frame::PixelBuffer(pixel_buffer) = frame else { 144 | eprintln!("Received frame is not PixelBuffer, skipping"); 145 | continue 146 | }; 147 | 148 | width = pixel_buffer.width(); 149 | height = pixel_buffer.height(); 150 | data = pixel_buffer.data().to_vec(); 151 | 152 | break; 153 | }; 154 | 155 | let pipeline = 156 | gst::parse::launch("appsrc name=input ! videoconvert ! pngenc ! appsink name=output").unwrap() 157 | .dynamic_cast::().unwrap(); 158 | 159 | pipeline.by_name("input").unwrap().dynamic_cast::().unwrap() 160 | .set_callbacks(gst_app::AppSrcCallbacks::builder() 161 | .need_data(move |source, _| { 162 | let caps = gst::Caps::builder("video/x-raw") 163 | .field("format", "BGRx") 164 | .field("width", width as i32) 165 | .field("height", height as i32) 166 | .build(); 167 | 168 | source.set_caps(Some(&caps)); 169 | 170 | let buffer = gst::Buffer::from_slice(data.clone()); 171 | 172 | let _ = source.push_buffer(buffer); 173 | 174 | source.end_of_stream().unwrap(); 175 | }).build()); 176 | 177 | let preview = Arc::new(Mutex::new(None)); 178 | 179 | pipeline.by_name("output").unwrap().dynamic_cast::().unwrap() 180 | .set_callbacks(gst_app::AppSinkCallbacks::builder() 181 | .new_sample({ 182 | let preview = preview.clone(); 183 | 184 | move |sink| { 185 | let Ok(sample) = sink.pull_sample() else { return Err(gst::FlowError::Error) }; 186 | 187 | let buffer = sample.buffer().unwrap(); 188 | let mapped_buffer = buffer.map_readable().unwrap(); 189 | 190 | let encoded_preview = mapped_buffer.as_slice(); 191 | 192 | *preview.lock().unwrap() = Some(encoded_preview.to_vec()); 193 | 194 | Ok(gst::FlowSuccess::Ok) 195 | } 196 | }).build()); 197 | 198 | crate::util::gstreamer_loop(pipeline, |_| { false })?; 199 | 200 | let preview = preview.lock().unwrap(); 201 | 202 | Ok(preview.clone().unwrap()) 203 | } 204 | 205 | pub fn all() -> anyhow::Result> { 206 | Ok(list_screen()?.into_iter() 207 | .map(|d| Self { name: d.name(), display: d }).collect()) 208 | } 209 | } 210 | 211 | -------------------------------------------------------------------------------- /src-tauri/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Div as _; 2 | 3 | use tauri::Window; 4 | 5 | pub fn format_timestamp(seconds: i64, always_include_hours: bool, decimal_marker: &str) -> String { 6 | assert!(seconds >= 0, "non-negative timestamp expected"); 7 | let mut milliseconds = (seconds * 10) as f32; 8 | 9 | let hours = milliseconds.div(3_600_000.0).floor(); 10 | milliseconds -= hours * 3_600_000.0; 11 | 12 | let minutes = milliseconds.div(60_000.0).floor(); 13 | milliseconds -= minutes * 60_000.0; 14 | 15 | let seconds = milliseconds.div(1_000.0).floor(); 16 | milliseconds -= seconds * 1_000.0; 17 | 18 | let hours_marker = if always_include_hours || hours as usize != 0 { 19 | format!("{hours}:") 20 | } else { 21 | String::new() 22 | }; 23 | 24 | format!("{hours_marker}{minutes:02}:{seconds:02}{decimal_marker}{milliseconds:03}") 25 | } 26 | 27 | pub fn emit_all( 28 | window: &Window, 29 | channel: impl AsRef, 30 | payload: S, 31 | ) { 32 | println!("Emitting {payload:?}"); 33 | 34 | let channel = channel.as_ref().to_owned(); 35 | 36 | window.emit(channel.as_str(), payload).unwrap(); 37 | } 38 | 39 | pub fn gstreamer_loop( 40 | pipeline: gst::Pipeline, 41 | on_message: impl Fn(&gst::Message) -> bool, 42 | ) -> anyhow::Result<()> { 43 | use gst::prelude::*; 44 | 45 | pipeline.set_state(gst::State::Playing)?; 46 | 47 | let bus = pipeline.bus().unwrap(); 48 | 49 | for message in bus.iter_timed(gst::ClockTime::NONE) { 50 | match message.view() { 51 | gst::MessageView::Eos(_) => break, 52 | gst::MessageView::Error(err) => anyhow::bail!(format!("{:?}", err)), 53 | _ => {} 54 | } 55 | 56 | let should_break = on_message(&message); 57 | if should_break { 58 | break; 59 | }; 60 | } 61 | 62 | pipeline.set_state(gst::State::Null)?; 63 | 64 | Ok(()) 65 | } 66 | 67 | pub fn replace_multiple_whitespace(input: &str) -> String { 68 | let mut result = String::new(); 69 | let mut last_was_whitespace = false; 70 | 71 | for c in input.chars() { 72 | if c.is_whitespace() { 73 | if !last_was_whitespace { 74 | result.push(' '); 75 | last_was_whitespace = true; 76 | } 77 | } else { 78 | result.push(c); 79 | last_was_whitespace = false; 80 | } 81 | } 82 | 83 | result 84 | } 85 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "npm run dev", 4 | "beforeBuildCommand": "npm run build", 5 | "devPath": "http://localhost:1420", 6 | "distDir": "../dist" 7 | }, 8 | "package": { 9 | "productName": "Recordscript", 10 | "version": "0.0.1" 11 | }, 12 | "tauri": { 13 | "allowlist": { 14 | "all": true, 15 | "notification": { 16 | "all": true 17 | }, 18 | "shell": { 19 | "all": true 20 | }, 21 | "fs": { 22 | "readFile": true, 23 | "scope": [ 24 | "**" 25 | ] 26 | } 27 | }, 28 | "windows": [ 29 | { 30 | "label": "main", 31 | "fullscreen": false, 32 | "resizable": true, 33 | "title": "Recordscript", 34 | "width": 800, 35 | "height": 700, 36 | "minWidth": 800, 37 | "minHeight": 700 38 | }, 39 | { 40 | "label": "recorder-controller", 41 | "fullscreen": false, 42 | "resizable": false, 43 | "maximizable": false, 44 | "title": "Recorder Controller", 45 | "width": 500, 46 | "height": 100, 47 | "decorations": false, 48 | "transparent": true, 49 | "skipTaskbar": true, 50 | "alwaysOnTop": true, 51 | "contentProtected": true, 52 | "visible": false, 53 | "url": "record-control.html" 54 | } 55 | ], 56 | "security": { 57 | "csp": null 58 | }, 59 | "bundle": { 60 | "active": true, 61 | "targets": "all", 62 | "identifier": "com.recordscript.Recordscript", 63 | "icon": [ 64 | "icons/32x32.png", 65 | "icons/128x128.png", 66 | "icons/128x128@2x.png", 67 | "icons/icon.icns", 68 | "icons/icon.ico" 69 | ] 70 | }, 71 | "updater": { 72 | "active": true, 73 | "dialog": true, 74 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEEzQkIxQTEyRTVFNjc3ODcKUldTSGQrYmxFaHE3b3p4T0pzbk5OVmdBdDlROU40Tm5nS1ZNck1DQUhGUll5dHBnYmlpd3NZWEIK", 75 | "endpoints": [ 76 | "https://fs-transcriber.s3.ap-southeast-1.amazonaws.com/release.json" 77 | ], 78 | "windows": { 79 | "installMode": "passive", 80 | "installerArgs": [] 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src-tauri/tauri.windows.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "tauri": { 3 | "bundle": { 4 | "resources": { 5 | "../libs/prebuilt/windows/*.dll": "" 6 | } 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/RecordControl.tsx: -------------------------------------------------------------------------------- 1 | import { invoke } from "@tauri-apps/api"; 2 | 3 | export default function() { 4 | const recording = { 5 | start: async function() { 6 | await invoke("start_record"); 7 | }, 8 | stop: async function() { 9 | await invoke("stop_record"); 10 | }, 11 | pause: async function() { 12 | await invoke("pause_record"); 13 | }, 14 | resume: async function() { 15 | await invoke("resume_record"); 16 | }, 17 | }; 18 | 19 | return ( 20 |
21 |

Recordscript is recording

22 | 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/github-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Recordscript/recordscript/ddd5f09045f8440d7d9b21373c03e32fad339e5c/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "smtp_article_url": "https://docs.google.com/document/d/1Tp8YbI9HxRnwQdYaRUC-QtPgdTjvK5vLYa0eVUoSU1c", 3 | "support_url": "https://forms.gle/FkG2KzP21BaPD6no9", 4 | "github_repository_url": "https://github.com/Recordscript", 5 | "homepage_url": "https://recordscript.com/" 6 | } 7 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | import { render } from "solid-js/web"; 3 | 4 | import App from "./App"; 5 | import "./styles.css"; 6 | 7 | render(() => , document.getElementById("root") as HTMLElement); 8 | -------------------------------------------------------------------------------- /src/lang.json: -------------------------------------------------------------------------------- 1 | [ 2 | ["Afrikaans", "af"], 3 | ["Albanian", "sq"], 4 | ["Amharic", "am"], 5 | ["Arabic", "ar"], 6 | ["Armenian", "hy"], 7 | ["Assamese", "as"], 8 | ["Aymara", "ay"], 9 | ["Azerbaijani", "az"], 10 | ["Bambara", "bm"], 11 | ["Basque", "eu"], 12 | ["Belarusian", "be"], 13 | ["Bengali", "bn"], 14 | ["Bhojpuri", "bho"], 15 | ["Bosnian", "bs"], 16 | ["Bulgarian", "bg"], 17 | ["Catalan", "ca"], 18 | ["Cebuano", "ceb"], 19 | ["Chinese (Simplified)", "zh-CN"], 20 | ["Chinese (Traditional)", "zh-TW"], 21 | ["Corsican", "co"], 22 | ["Croatian", "hr"], 23 | ["Czech", "cs"], 24 | ["Danish", "da"], 25 | ["Dhivehi", "dv"], 26 | ["Dogri", "doi"], 27 | ["Dutch", "nl"], 28 | ["English", "en"], 29 | ["Esperanto", "eo"], 30 | ["Estonian", "et"], 31 | ["Ewe", "ee"], 32 | ["Filipino (Tagalog)", "fil"], 33 | ["Finnish", "fi"], 34 | ["French", "fr"], 35 | ["Frisian", "fy"], 36 | ["Galician", "gl"], 37 | ["Georgian", "ka"], 38 | ["German", "de"], 39 | ["Greek", "el"], 40 | ["Guarani", "gn"], 41 | ["Gujarati", "gu"], 42 | ["Haitian Creole", "ht"], 43 | ["Hausa", "ha"], 44 | ["Hawaiian", "haw"], 45 | ["Hebrew", "he"], 46 | ["Hindi", "hi"], 47 | ["Hmong", "hmn"], 48 | ["Hungarian", "hu"], 49 | ["Icelandic", "is"], 50 | ["Igbo", "ig"], 51 | ["Ilocano", "ilo"], 52 | ["Indonesian", "id"], 53 | ["Irish", "ga"], 54 | ["Italian", "it"], 55 | ["Japanese", "ja"], 56 | ["Javanese", "jw"], 57 | ["Kannada", "kn"], 58 | ["Kazakh", "kk"], 59 | ["Khmer", "km"], 60 | ["Kinyarwanda", "rw"], 61 | ["Konkani", "gom"], 62 | ["Korean", "ko"], 63 | ["Krio", "kri"], 64 | ["Kurdish", "ku"], 65 | ["Kurdish (Sorani)", "ckb"], 66 | ["Kyrgyz", "ky"], 67 | ["Lao", "lo"], 68 | ["Latin", "la"], 69 | ["Latvian", "lv"], 70 | ["Lingala", "ln"], 71 | ["Lithuanian", "lt"], 72 | ["Luganda", "lg"], 73 | ["Luxembourgish", "lb"], 74 | ["Macedonian", "mk"], 75 | ["Maithili", "mai"], 76 | ["Malagasy", "mg"], 77 | ["Malay", "ms"], 78 | ["Malayalam", "ml"], 79 | ["Maltese", "mt"], 80 | ["Maori", "mi"], 81 | ["Marathi", "mr"], 82 | ["Meiteilon (Manipuri)", "mni-Mtei"], 83 | ["Mizo", "lus"], 84 | ["Mongolian", "mn"], 85 | ["Myanmar (Burmese)", "my"], 86 | ["Nepali", "ne"], 87 | ["Norwegian", "no"], 88 | ["Nyanja (Chichewa)", "ny"], 89 | ["Odia (Oriya)", "or"], 90 | ["Oromo", "om"], 91 | ["Pashto", "ps"], 92 | ["Persian", "fa"], 93 | ["Polish", "pl"], 94 | ["Portuguese (Portugal, Brazil)", "pt"], 95 | ["Punjabi", "pa"], 96 | ["Quechua", "qu"], 97 | ["Romanian", "ro"], 98 | ["Russian", "ru"], 99 | ["Samoan", "sm"], 100 | ["Sanskrit", "sa"], 101 | ["Scots Gaelic", "gd"], 102 | ["Sepedi", "nso"], 103 | ["Serbian", "sr"], 104 | ["Sesotho", "st"], 105 | ["Shona", "sn"], 106 | ["Sindhi", "sd"], 107 | ["Sinhala (Sinhalese)", "si"], 108 | ["Slovak", "sk"], 109 | ["Slovenian", "sl"], 110 | ["Somali", "so"], 111 | ["Spanish", "es"], 112 | ["Sundanese", "su"], 113 | ["Swahili", "sw"], 114 | ["Swedish", "sv"], 115 | ["Tagalog (Filipino)", "tl"], 116 | ["Tajik", "tg"], 117 | ["Tamil", "ta"], 118 | ["Tatar", "tt"], 119 | ["Telugu", "te"], 120 | ["Thai", "th"], 121 | ["Tigrinya", "ti"], 122 | ["Tsonga", "ts"], 123 | ["Turkish", "tr"], 124 | ["Turkmen", "tk"], 125 | ["Twi (Akan)", "ak"], 126 | ["Ukrainian", "uk"], 127 | ["Urdu", "ur"], 128 | ["Uyghur", "ug"], 129 | ["Uzbek", "uz"], 130 | ["Vietnamese", "vi"], 131 | ["Welsh", "cy"], 132 | ["Xhosa", "xh"], 133 | ["Yiddish", "yi"], 134 | ["Yoruba", "yo"], 135 | ["Zulu", "zu"] 136 | ] 137 | -------------------------------------------------------------------------------- /src/record-control.tsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | import { render } from "solid-js/web"; 3 | 4 | import App from "./RecordControl"; 5 | import "./styles.css"; 6 | 7 | render(() => , document.getElementById("root") as HTMLElement); 8 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | margin: 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | // https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.1 2 | export function validate_domain_name(name: string): boolean { 3 | if (name.length > 253) return false; 4 | 5 | const labels = name.split("."); 6 | if (labels.some((label) => label.length === 0 || label.length > 63)) return false; 7 | 8 | const label_regex = /^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/; 9 | if (!labels.every((label) => label_regex.test(label))) return false; 10 | 11 | return true; 12 | } 13 | 14 | export function validate_port(port: number): boolean { 15 | if (!Number.isInteger(port)) return false; 16 | 17 | return port >= 0 && port <= 65535; 18 | } 19 | 20 | // https://datatracker.ietf.org/doc/html/rfc5322#section-3.4.1 21 | export function validate_email(email: string): boolean { 22 | const parts = email.split("@"); 23 | if (parts.length !== 2) return false; 24 | 25 | const [local, domain] = parts; 26 | 27 | function is_valid_local(local: string) { 28 | if (local.length > 64) return false; 29 | 30 | const valid_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-/=?^_`{|}~."; 31 | 32 | for (const char of local) { 33 | if (!valid_chars.includes(char)) { 34 | return false; 35 | } 36 | } 37 | 38 | return true; 39 | } 40 | 41 | return is_valid_local(local) && validate_domain_name(domain); 42 | } 43 | 44 | // https://datatracker.ietf.org/doc/html/rfc1036#section-2.1.1 45 | export function validate_email_from_header(from: string): boolean { 46 | const angle_email_regex = /<([^>]+)>/; 47 | const paren_name_regex = /\(([^)]+)\)/; 48 | 49 | let email, name; 50 | 51 | if (angle_email_regex.test(from)) { 52 | email = from.match(angle_email_regex)?.[1]; 53 | name = from.split("<")[0].trim(); 54 | } else if (paren_name_regex.test(from)) { 55 | email = from.split("(")[0].trim(); 56 | name = from.match(paren_name_regex)?.[1]; 57 | } else { 58 | email = from.trim(); 59 | } 60 | 61 | function is_valid_name(name: string) { 62 | const invalid_chars = "()<>"; 63 | 64 | for (const char of name) { 65 | if (invalid_chars.includes(char)) { 66 | return false; 67 | } 68 | } 69 | 70 | return true; 71 | } 72 | 73 | return validate_email(email ?? "") && (!name || is_valid_name(name)); 74 | } 75 | 76 | export function extract_file_path(path: string) { 77 | return path.replace(/[^\\/]*$/, ""); 78 | } 79 | 80 | export function megabytes_to_jedec_string(megabytes: number): string { 81 | const units = ["MB", "GB", "TB", "PB", "EB"]; 82 | 83 | let size = megabytes; 84 | let unit_index = 0; 85 | 86 | while (size >= 1024 && unit_index < units.length - 1) { 87 | size /= 1024; 88 | unit_index++; 89 | } 90 | 91 | return `${size.toFixed(2)} ${units[unit_index]}`; 92 | } 93 | 94 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | "jsxImportSource": "solid-js", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | 24 | "types": ["vite/client"] 25 | }, 26 | "include": ["src"], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import solid from "vite-plugin-solid"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig(async () => ({ 6 | plugins: [solid()], 7 | 8 | build: { 9 | rollupOptions: { 10 | input: { 11 | main: "./index.html", 12 | record_controller: "./record-control.html" 13 | } 14 | } 15 | }, 16 | 17 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 18 | // 19 | // 1. prevent vite from obscuring rust errors 20 | clearScreen: false, 21 | // 2. tauri expects a fixed port, fail if that port is not available 22 | server: { 23 | port: 1420, 24 | strictPort: true, 25 | watch: { 26 | // 3. tell vite to ignore watching `src-tauri` 27 | ignored: ["**/src-tauri/**"], 28 | }, 29 | headers: { 30 | "Cross-Origin-Opener-Policy": "same-origin", 31 | "Cross-Origin-Embedder-Policy": "require-corp", 32 | }, 33 | }, 34 | optimizeDeps: { 35 | exclude: ["@ffmpeg/ffmpeg", "@ffmpeg/util"], 36 | }, 37 | })); 38 | --------------------------------------------------------------------------------