diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 00000000..2664a1b2 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,164 @@ +name: Rust CI + +on: + push: + branches: [ main, develop ] + paths: + - 'bindings/rust/**' + - 'src/**' + - 'include/**' + - 'CMakeLists.txt' + - '.github/workflows/rust.yml' + pull_request: + branches: [ main, develop ] + paths: + - 'bindings/rust/**' + - 'src/**' + - 'include/**' + - 'CMakeLists.txt' + - '.github/workflows/rust.yml' + +env: + CARGO_TERM_COLOR: always + CCAP_SKIP_CAMERA_TESTS: 1 + +permissions: + contents: read + +jobs: + # Job 1: Static Linking (Development Mode) + # Verifies that the crate links correctly against a pre-built C++ library. + static-link: + name: Static Link (Dev) + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install system dependencies (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y cmake build-essential pkg-config libclang-dev + + - name: Install system dependencies (Windows) + if: matrix.os == 'windows-latest' + run: | + choco install cmake llvm -y + + - name: Configure LIBCLANG_PATH (Windows) + if: matrix.os == 'windows-latest' + shell: pwsh + run: echo "LIBCLANG_PATH=C:\\Program Files\\LLVM\\bin" >> $env:GITHUB_ENV + + - name: Install system dependencies (macOS) + if: matrix.os == 'macos-latest' + run: | + brew install cmake llvm + echo "LIBCLANG_PATH=$(brew --prefix llvm)/lib" >> $GITHUB_ENV + + - name: Install Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + components: clippy, rustfmt + cache: false + + # Build C++ Library (Linux/macOS) + # We build in build/Debug to match build.rs expectations for Unix Makefiles + - name: Build C library (Unix) + if: runner.os != 'Windows' + run: | + mkdir -p build/Debug + cd build/Debug + cmake -DCMAKE_BUILD_TYPE=Debug -DCCAP_BUILD_EXAMPLES=OFF -DCCAP_BUILD_TESTS=OFF ../.. + cmake --build . --config Debug --parallel + + # Build C++ Library (Windows) + # Build both Debug and Release versions + # MSVC is a multi-config generator, so we build to build/ and specify config at build time + - name: Build C library (Windows) + if: runner.os == 'Windows' + run: | + mkdir build + cd build + cmake -DCCAP_BUILD_EXAMPLES=OFF -DCCAP_BUILD_TESTS=OFF .. + cmake --build . --config Debug --parallel + cmake --build . --config Release --parallel + + - name: Check formatting + if: matrix.os == 'ubuntu-latest' + working-directory: bindings/rust + run: cargo fmt -- --check + + - name: Run clippy + if: matrix.os == 'ubuntu-latest' + working-directory: bindings/rust + run: cargo clippy --all-targets --features static-link -- -D warnings + + - name: Build Rust bindings + working-directory: bindings/rust + run: cargo build --verbose --features static-link + + - name: Run tests + working-directory: bindings/rust + run: cargo test --verbose --features static-link + + # Job 2: Source Build (Distribution Mode) + # Verifies that the crate builds correctly from source using the cc crate. + # This is crucial for crates.io distribution. + build-source: + name: Build Source (Dist) + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install system dependencies (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config + + - name: Install system dependencies (macOS) + if: matrix.os == 'macos-latest' + run: brew install llvm + + - name: Install system dependencies (Windows) + if: matrix.os == 'windows-latest' + run: | + choco install llvm -y + refreshenv + + - name: Configure LIBCLANG_PATH (Windows) + if: matrix.os == 'windows-latest' + shell: pwsh + run: echo "LIBCLANG_PATH=C:\\Program Files\\LLVM\\bin" >> $env:GITHUB_ENV + + - name: Configure LIBCLANG_PATH (macOS) + if: matrix.os == 'macos-latest' + run: echo "LIBCLANG_PATH=$(brew --prefix llvm)/lib" >> $GITHUB_ENV + + - name: Install Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: false + # The build.rs script should handle it via the 'build-source' feature. + + - name: Build Rust bindings (Source) + working-directory: bindings/rust + # Disable default features (static-link) and enable build-source + run: cargo build --verbose --no-default-features --features build-source + + - name: Run tests (Source) + working-directory: bindings/rust + run: cargo test --verbose --no-default-features --features build-source diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b7b455ab..73e7ff89 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1304,6 +1304,106 @@ "problemMatcher": "$msCompile" } }, + { + "label": "Run Rust print_camera", + "type": "shell", + "command": "cargo", + "args": [ + "run", + "--example", + "print_camera" + ], + "options": { + "cwd": "${workspaceFolder}/bindings/rust" + }, + "group": "build", + "problemMatcher": "$rustc" + }, + { + "label": "Run Rust minimal_example", + "type": "shell", + "command": "cargo", + "args": [ + "run", + "--example", + "minimal_example" + ], + "options": { + "cwd": "${workspaceFolder}/bindings/rust" + }, + "group": "build", + "problemMatcher": "$rustc" + }, + { + "label": "Run Rust capture_grab", + "type": "shell", + "command": "cargo", + "args": [ + "run", + "--example", + "capture_grab" + ], + "options": { + "cwd": "${workspaceFolder}/bindings/rust" + }, + "group": "build", + "problemMatcher": "$rustc" + }, + { + "label": "Run Rust capture_callback", + "type": "shell", + "command": "cargo", + "args": [ + "run", + "--example", + "capture_callback" + ], + "options": { + "cwd": "${workspaceFolder}/bindings/rust" + }, + "group": "build", + "problemMatcher": "$rustc" + }, + { + "label": "Build Rust Bindings", + "type": "shell", + "command": "cargo", + "args": [ + "build" + ], + "options": { + "cwd": "${workspaceFolder}/bindings/rust" + }, + "group": "build", + "problemMatcher": "$rustc" + }, + { + "label": "Build Rust Examples", + "type": "shell", + "command": "cargo", + "args": [ + "build", + "--examples" + ], + "options": { + "cwd": "${workspaceFolder}/bindings/rust" + }, + "group": "build", + "problemMatcher": "$rustc" + }, + { + "label": "Test Rust Bindings", + "type": "shell", + "command": "cargo", + "args": [ + "test" + ], + "options": { + "cwd": "${workspaceFolder}/bindings/rust" + }, + "group": "build", + "problemMatcher": "$rustc" + }, { "label": "Run ccap CLI --video test.mp4 (Debug)", "type": "shell", diff --git a/CMakeLists.txt b/CMakeLists.txt index 0db213fb..857b46e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -373,4 +373,37 @@ if (CCAP_INSTALL) message(STATUS " Libraries: ${CMAKE_INSTALL_PREFIX}/${CCAP_INSTALL_LIBDIR}") message(STATUS " Headers: ${CMAKE_INSTALL_PREFIX}/${CCAP_INSTALL_INCLUDEDIR}") message(STATUS " CMake: ${CMAKE_INSTALL_PREFIX}/${CCAP_INSTALL_CMAKEDIR}") -endif () \ No newline at end of file +endif () + +# ############### Rust Bindings ################ +option(CCAP_BUILD_RUST "Build Rust bindings" OFF) + +if (CCAP_BUILD_RUST) + message(STATUS "ccap: Building Rust bindings") + # Find Rust/Cargo + find_program(CARGO_CMD cargo) + if (NOT CARGO_CMD) + message(WARNING "cargo not found - Rust bindings disabled") + else () + message(STATUS "ccap: Found cargo: ${CARGO_CMD}") + # Add custom target to build Rust bindings + add_custom_target(ccap-rust + COMMAND ${CARGO_CMD} build --release + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bindings/rust + COMMENT "Building Rust bindings" + DEPENDS ccap + ) + # Add custom target to test Rust bindings + add_custom_target(ccap-rust-test + COMMAND ${CARGO_CMD} test + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bindings/rust + COMMENT "Testing Rust bindings" + DEPENDS ccap-rust + ) + # Rust bindings are optional, do not add to main build automatically + # Users can explicitly build with: cmake --build . --target ccap-rust + message(STATUS "ccap: Rust bindings targets added:") + message(STATUS " ccap-rust: Build Rust bindings") + message(STATUS " ccap-rust-test: Test Rust bindings") + endif () +endif () diff --git a/bindings/rust/.gitignore b/bindings/rust/.gitignore new file mode 100644 index 00000000..d59276fb --- /dev/null +++ b/bindings/rust/.gitignore @@ -0,0 +1,2 @@ +target/ +image_capture/ diff --git a/bindings/rust/Cargo.lock b/bindings/rust/Cargo.lock new file mode 100644 index 00000000..a7dde97f --- /dev/null +++ b/bindings/rust/Cargo.lock @@ -0,0 +1,835 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "bindgen" +version = "0.68.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "shlex", +] + +[[package]] +name = "ccap" +version = "1.5.0" +dependencies = [ + "bindgen", + "cc", + "futures", + "thiserror", + "tokio", + "tokio-stream", + "tokio-test", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.3", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.59.0", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml new file mode 100644 index 00000000..88025137 --- /dev/null +++ b/bindings/rust/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "ccap" +version = "1.5.0" +edition = "2021" +rust-version = "1.65" +authors = ["wysaid "] +license = "MIT" +description = "Rust bindings for ccap - A high-performance, lightweight cross-platform camera capture library" +homepage = "https://github.com/wysaid/CameraCapture" +repository = "https://github.com/wysaid/CameraCapture" +readme = "README.md" +keywords = ["camera", "capture", "video", "cross-platform"] +categories = ["multimedia::video", "api-bindings"] + +[dependencies] +thiserror = "1.0" +tokio = { version = "1.0", features = ["full"], optional = true } +tokio-stream = { version = "0.1", optional = true } +futures = { version = "0.3", optional = true } + +[build-dependencies] +bindgen = "0.68" +cc = "1.0" + +[dev-dependencies] +tokio-test = "0.4" + +[features] +default = ["static-link"] +async = ["tokio", "tokio-stream", "futures"] +static-link = [] # Link against pre-built static library (for development) +build-source = [] # Build from source using cc crate (for distribution) + +[[example]] +name = "print_camera" +path = "examples/print_camera.rs" + +[[example]] +name = "minimal_example" +path = "examples/minimal_example.rs" + +[[example]] +name = "capture_grab" +path = "examples/capture_grab.rs" + +[[example]] +name = "capture_callback" +path = "examples/capture_callback.rs" diff --git a/bindings/rust/README.md b/bindings/rust/README.md new file mode 100644 index 00000000..9e66f697 --- /dev/null +++ b/bindings/rust/README.md @@ -0,0 +1,196 @@ +# ccap-rs - Rust Bindings for CameraCapture + +[![Crates.io](https://img.shields.io/crates/v/ccap.svg)](https://crates.io/crates/ccap) +[![Documentation](https://docs.rs/ccap/badge.svg)](https://docs.rs/ccap) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +Safe Rust bindings for [ccap](https://github.com/wysaid/CameraCapture) - A high-performance, lightweight cross-platform camera capture library with hardware-accelerated pixel format conversion. + +## Features + +- **High Performance**: Hardware-accelerated pixel format conversion with up to 10x speedup (AVX2, Apple Accelerate, NEON) +- **Cross Platform**: Windows (DirectShow), macOS/iOS (AVFoundation), Linux (V4L2) +- **Multiple Formats**: RGB, BGR, YUV (NV12/I420) with automatic conversion +- **Zero Dependencies**: Uses only system frameworks +- **Memory Safe**: Safe Rust API with automatic resource management +- **Async Support**: Optional async/await interface with tokio +- **Thread Safe**: Safe concurrent access to video frames + +## Quick Start + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +ccap = "1.1.0" + +# For async support +ccap = { version = "1.1.0", features = ["async"] } +``` + +### Basic Usage + +```rust +use ccap::{Provider, Result}; + +fn main() -> Result<()> { + // Create a camera provider + let mut provider = Provider::new()?; + + // Find available cameras + let devices = provider.find_device_names()?; + println!("Found {} cameras:", devices.len()); + for (i, device) in devices.iter().enumerate() { + println!(" [{}] {}", i, device); + } + + // Open the first camera + if !devices.is_empty() { + provider.open_device(Some(&devices[0]), true)?; + println!("Camera opened successfully!"); + + // Capture a frame (with 3 second timeout) + if let Some(frame) = provider.grab_frame(3000)? { + let info = frame.info()?; + println!("Captured frame: {}x{}, format: {:?}", + info.width, info.height, info.pixel_format); + + // Access frame data + let data = frame.data()?; + println!("Frame data size: {} bytes", data.len()); + } + } + + Ok(()) +} +``` + +### Async Usage + +```rust +use ccap::r#async::AsyncProvider; + +#[tokio::main] +async fn main() -> ccap::Result<()> { + let provider = AsyncProvider::new().await?; + let devices = provider.find_device_names().await?; + + if !devices.is_empty() { + provider.open(Some(&devices[0]), true).await?; + + // Async frame capture + if let Some(frame) = provider.grab_frame().await? { + let info = frame.info()?; + println!("Async captured: {}x{}", info.width, info.height); + } + } + + Ok(()) +} +``` + +## Examples + +The crate includes several examples: + +```bash +# List available cameras and their info +cargo run --example print_camera + +# Minimal capture example +cargo run --example minimal_example + +# Capture frames using grab mode +cargo run --example capture_grab + +# Capture frames using callback mode +cargo run --example capture_callback + +# With async support +cargo run --features async --example capture_callback +``` + +## Building + +### Prerequisites + +Before building the Rust bindings, you need to build the ccap C library: + +```bash +# From the root of the CameraCapture project +./scripts/build_and_install.sh +``` + +Or manually: + +```bash +mkdir -p build/Debug +cd build/Debug +cmake ../.. -DCMAKE_BUILD_TYPE=Debug +make -j$(nproc) +``` + +### Building the Rust Crate + +```bash +# From bindings/rust directory +cargo build +cargo test +``` + +## API Documentation + +### Core Types + +- `Provider`: Main camera capture interface +- `VideoFrame`: Represents a captured video frame +- `DeviceInfo`: Camera device information +- `PixelFormat`: Supported pixel formats (RGB24, BGR24, NV12, I420, etc.) +- `Resolution`: Frame resolution specification + +### Error Handling + +All operations return `Result` for comprehensive error handling. +For frame capture, `grab_frame(timeout_ms)` returns `Result, CcapError>`: + +```rust +match provider.grab_frame(3000) { // 3 second timeout + Ok(Some(frame)) => { /* process frame */ }, + Ok(None) => println!("No frame available"), + Err(e) => eprintln!("Capture error: {:?}", e), +} +``` + +### Thread Safety + +- `VideoFrame` is `Send + Sync` and can be safely shared between threads +- `Provider` should be used from a single thread or protected with synchronization +- Frame data remains valid until the `VideoFrame` is dropped + +## Platform Support + +| Platform | Backend | Status | +|----------|---------|---------| +| Windows | DirectShow | ✅ Supported | +| macOS | AVFoundation | ✅ Supported | +| iOS | AVFoundation | ✅ Supported | +| Linux | V4L2 | ✅ Supported | + +## System Requirements + +- Rust 1.65+ +- CMake 3.14+ +- Platform-specific camera frameworks (automatically linked) + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details. + +## Related Projects + +- [ccap (C/C++)](https://github.com/wysaid/CameraCapture) - The underlying C/C++ library +- [OpenCV](https://opencv.org/) - Alternative computer vision library with camera support diff --git a/bindings/rust/RUST_INTEGRATION_SUMMARY.md b/bindings/rust/RUST_INTEGRATION_SUMMARY.md new file mode 100644 index 00000000..fbdd72c3 --- /dev/null +++ b/bindings/rust/RUST_INTEGRATION_SUMMARY.md @@ -0,0 +1,181 @@ +# Rust 绑定实现完成总结 + +## 项目概述 + +根据您的要求,我们已经成功为 ccap 相机捕捉库完成了 Rust 支持。整个实现过程按照您提出的三步计划进行: + +## ✅ 第一步:项目接口设计理解 + +经过分析,ccap 是一个跨平台的相机捕捉库: + +- **项目性质**:C++17 编写的零依赖相机库 +- **支持平台**:Windows (DirectShow)、macOS (AVFoundation)、Linux (V4L2) +- **核心功能**:相机设备枚举、视频帧捕获、像素格式转换 +- **硬件加速**:支持 AVX2、Apple Accelerate、NEON +- **接口层次**:提供 C++ 和 C 两套接口 + +## ✅ 第二步:C/C++ 接口兼容性评估 + +分析结果表明 C 接口完全符合 Rust FFI 规范: + +- **内存管理**:明确的创建/销毁函数配对 +- **错误处理**:基于枚举的错误码系统 +- **类型安全**:使用不透明指针避免 ABI 问题 +- **线程安全**:通过 C 接口天然避免 C++ 对象跨边界传递 + +## ✅ 第三步:Rust 接口实现 + +### 项目结构 +``` +bindings/rust/ +├── Cargo.toml # 包配置 +├── build.rs # 构建脚本 (bindgen 集成) +├── wrapper.h # C 头文件包装 +├── src/ +│ ├── lib.rs # 主库文件 +│ ├── error.rs # 错误处理 +│ ├── types.rs # 类型转换 +│ ├── frame.rs # 视频帧封装 +│ ├── provider.rs # 相机提供器 +│ └── async.rs # 异步支持 +├── examples/ # 示例程序 +│ ├── list_cameras.rs # 枚举相机 +│ ├── capture_frames.rs # 捕获帧 +│ └── async_capture.rs # 异步捕获 +├── README.md # 使用文档 +└── build_and_test.sh # 测试脚本 +``` + +### 核心功能实现 + +1. **类型安全的枚举** + - PixelFormat:像素格式 (NV12, I420, RGB24, 等) + - FrameOrientation:帧方向 + - PropertyName:属性名称 + +2. **内存安全的封装** + - Provider:相机提供器,自动管理生命周期 + - VideoFrame:视频帧,确保内存安全访问 + - DeviceInfo:设备信息,包含名称和支持的格式 + +3. **Rust 风格的错误处理** + - CcapError:完整的错误类型映射 + - Result 类型别名,符合 Rust 惯例 + - 使用 thiserror 提供友好的错误消息 + +4. **异步支持** (可选 feature) + - 基于 tokio 的异步接口 + - 流式 API for 连续帧捕获 + +### 构建系统集成 + +修改了项目根目录的 `CMakeLists.txt`: +```cmake +option(CCAP_BUILD_RUST "Build Rust bindings" OFF) + +if(CCAP_BUILD_RUST) + add_custom_target(ccap-rust + COMMAND cargo build --release + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bindings/rust + COMMENT "Building Rust bindings" + DEPENDS ccap + ) + + add_custom_target(ccap-rust-test + COMMAND cargo test + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bindings/rust + COMMENT "Testing Rust bindings" + DEPENDS ccap-rust + ) +endif() +``` + +### 使用示例 + +```rust +use ccap::{Provider, PixelFormat, Resolution}; + +fn main() -> ccap::Result<()> { + // 枚举设备 + let devices = Provider::enumerate_devices()?; + println!("Found {} cameras", devices.len()); + + // 创建提供器并打开设备 + let mut provider = Provider::with_device(&devices[0])?; + provider.open()?; + + // 设置属性 + provider.set_resolution(Resolution { width: 640, height: 480 })?; + provider.set_pixel_format(PixelFormat::Rgb24)?; + provider.set_frame_rate(30.0)?; + + // 开始捕获 + provider.start()?; + + // 捕获帧 + let frame = provider.grab_frame(1000)?; // 1秒超时 + println!("Captured frame: {}x{}", frame.width, frame.height); + + Ok(()) +} +``` + +### 测试结果 + +所有单元测试通过: +```bash +$ cargo test --lib +running 7 tests +test tests::test_pixel_format_conversion ... ok +test tests::test_error_conversion ... ok +test tests::test_constants ... ok +test sys::bindgen_test_layout_CcapDeviceInfo ... ok +test sys::bindgen_test_layout_CcapDeviceNamesList ... ok +test sys::bindgen_test_layout_CcapResolution ... ok +test sys::bindgen_test_layout_CcapVideoFrameInfo ... ok + +test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out +``` + +## 技术特点 + +1. **零拷贝数据访问**:视频帧数据通过切片直接访问,无需额外拷贝 +2. **RAII 资源管理**:所有资源在 Drop 时自动释放 +3. **类型安全**:强类型系统防止运行时错误 +4. **跨平台**:支持 macOS、Windows、Linux +5. **文档完善**:包含使用示例和 API 文档 + +## 部署说明 + +### 构建需求 +- Rust 1.70+ +- CMake 3.14+ +- 支持的 C++ 编译器 +- bindgen 依赖:clang/libclang + +### 使用方式 +```toml +[dependencies] +ccap = { path = "../path/to/ccap/bindings/rust" } + +# 或者启用异步功能 +ccap = { path = "../path/to/ccap/bindings/rust", features = ["async"] } +``` + +## 总结 + +✅ **完成了您的三步要求**: +1. ✅ 检查并理解项目接口设计 +2. ✅ 评估 C/C++ 接口与 Rust FFI 的兼容性 +3. ✅ 实现完整的 Rust 接口绑定 + +✅ **实现的特性**: +- 类型安全的 Rust 封装 +- 内存安全的资源管理 +- 完整的错误处理 +- 跨平台支持 +- 异步 API 支持 +- 丰富的示例和文档 +- 单元测试覆盖 + +您现在可以在 Rust 项目中方便地使用 ccap 库进行相机捕获操作了! diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs new file mode 100644 index 00000000..e2523b8b --- /dev/null +++ b/bindings/rust/build.rs @@ -0,0 +1,229 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + // Tell cargo to look for shared libraries in the specified directory + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let manifest_path = PathBuf::from(&manifest_dir); + + // Locate ccap root: + // 1. Check for local "native" directory (Packaged/Crates.io mode) + // 2. Fallback to "../../" (Repo/Git mode) + let (ccap_root, is_packaged) = if manifest_path.join("native").exists() { + (manifest_path.join("native"), true) + } else { + ( + manifest_path + .parent() + .and_then(|p| p.parent()) + .expect("Cargo manifest must be at least 2 directories deep (bindings/rust/)") + .to_path_buf(), + false, + ) + }; + + // Check if we should build from source or link against pre-built library + let build_from_source = env::var("CARGO_FEATURE_BUILD_SOURCE").is_ok(); + + if build_from_source { + // Build from source using cc crate + let mut build = cc::Build::new(); + + // Add source files (excluding SIMD-specific files) + build + .file(ccap_root.join("src/ccap_core.cpp")) + .file(ccap_root.join("src/ccap_utils.cpp")) + .file(ccap_root.join("src/ccap_convert.cpp")) + .file(ccap_root.join("src/ccap_convert_frame.cpp")) + .file(ccap_root.join("src/ccap_imp.cpp")) + .file(ccap_root.join("src/ccap_c.cpp")) + .file(ccap_root.join("src/ccap_utils_c.cpp")) + .file(ccap_root.join("src/ccap_convert_c.cpp")); + + // Platform specific sources + #[cfg(target_os = "macos")] + { + build + .file(ccap_root.join("src/ccap_imp_apple.mm")) + .file(ccap_root.join("src/ccap_convert_apple.cpp")) + .file(ccap_root.join("src/ccap_file_reader_apple.mm")); + } + + #[cfg(target_os = "linux")] + { + build.file(ccap_root.join("src/ccap_imp_linux.cpp")); + } + + #[cfg(target_os = "windows")] + { + build + .file(ccap_root.join("src/ccap_imp_windows.cpp")) + .file(ccap_root.join("src/ccap_file_reader_windows.cpp")); + } + + // Include directories + build + .include(ccap_root.join("include")) + .include(ccap_root.join("src")); + + // Compiler flags + build.cpp(true).std("c++17"); // Use C++17 + + // Enable file playback support + build.define("CCAP_ENABLE_FILE_PLAYBACK", "1"); + + #[cfg(target_os = "macos")] + { + build.flag("-fobjc-arc"); // Enable ARC for Objective-C++ + } + + // Compile + build.compile("ccap"); + + // Build SIMD-specific files separately with appropriate flags + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + let mut avx2_build = cc::Build::new(); + avx2_build + .file(ccap_root.join("src/ccap_convert_avx2.cpp")) + .include(ccap_root.join("include")) + .include(ccap_root.join("src")) + .cpp(true) + .std("c++17"); + + // Only add SIMD flags on non-MSVC compilers + if !avx2_build.get_compiler().is_like_msvc() { + avx2_build.flag("-mavx2").flag("-mfma"); + } else { + // MSVC uses /arch:AVX2 + avx2_build.flag("/arch:AVX2"); + } + + avx2_build.compile("ccap_avx2"); + } + + // Always build neon file for hasNEON() symbol + // On non-ARM architectures, ENABLE_NEON_IMP will be 0 and function returns false + { + let mut neon_build = cc::Build::new(); + neon_build + .file(ccap_root.join("src/ccap_convert_neon.cpp")) + .include(ccap_root.join("include")) + .include(ccap_root.join("src")) + .cpp(true) + .std("c++17"); + + // Only add NEON flags on aarch64 + #[cfg(target_arch = "aarch64")] + { + // NEON is always available on aarch64, no special flags needed + } + + neon_build.compile("ccap_neon"); + } + + println!("cargo:warning=Building ccap from source..."); + } else { + // Link against pre-built library (Development mode) + // Determine build profile + let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".to_string()); + let build_type = if profile == "release" { + "Release" + } else { + "Debug" + }; + + // Add the ccap library search path + // Try specific build type first, then fallback to others + println!( + "cargo:rustc-link-search=native={}/build/{}", + ccap_root.display(), + build_type + ); + println!( + "cargo:rustc-link-search=native={}/build/Debug", + ccap_root.display() + ); + println!( + "cargo:rustc-link-search=native={}/build/Release", + ccap_root.display() + ); + + // Link to ccap library + // Note: On MSVC, we always link to the Release version (ccap.lib) + // to avoid CRT mismatch issues, since Rust uses the release CRT + // even in debug builds by default + println!("cargo:rustc-link-lib=static=ccap"); + + println!("cargo:warning=Linking against pre-built ccap library (dev mode)..."); + } + + // Platform-specific linking (Common for both modes) + #[cfg(target_os = "macos")] + { + println!("cargo:rustc-link-lib=framework=Foundation"); + println!("cargo:rustc-link-lib=framework=AVFoundation"); + println!("cargo:rustc-link-lib=framework=CoreMedia"); + println!("cargo:rustc-link-lib=framework=CoreVideo"); + println!("cargo:rustc-link-lib=framework=Accelerate"); + println!("cargo:rustc-link-lib=System"); + println!("cargo:rustc-link-lib=c++"); + } + + #[cfg(target_os = "linux")] + { + // v4l2 might not be available on all systems + // println!("cargo:rustc-link-lib=v4l2"); + println!("cargo:rustc-link-lib=stdc++"); + } + + #[cfg(target_os = "windows")] + { + println!("cargo:rustc-link-lib=strmiids"); + println!("cargo:rustc-link-lib=ole32"); + println!("cargo:rustc-link-lib=oleaut32"); + // Media Foundation libraries for video file playback + println!("cargo:rustc-link-lib=mfplat"); + println!("cargo:rustc-link-lib=mfreadwrite"); + println!("cargo:rustc-link-lib=mfuuid"); + } + + // Tell cargo to invalidate the built crate whenever the wrapper changes + println!("cargo:rerun-if-changed=wrapper.h"); + // Use ccap_root for include paths to work in both packaged and repo modes + if !is_packaged { + println!( + "cargo:rerun-if-changed={}/include/ccap_c.h", + ccap_root.display() + ); + println!( + "cargo:rerun-if-changed={}/include/ccap_utils_c.h", + ccap_root.display() + ); + println!( + "cargo:rerun-if-changed={}/include/ccap_convert_c.h", + ccap_root.display() + ); + } + + // Generate bindings + let bindings = bindgen::Builder::default() + .header("wrapper.h") + .clang_arg(format!("-I{}/include", ccap_root.display())) + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + .allowlist_function("ccap_.*") + .allowlist_type("Ccap.*") + .allowlist_var("CCAP_.*") + .derive_default(true) + .derive_debug(true) + .derive_partialeq(true) + .derive_eq(true) + .generate() + .expect("Unable to generate bindings"); + + // Write the bindings to the $OUT_DIR/bindings.rs file. + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/bindings/rust/build_and_test.sh b/bindings/rust/build_and_test.sh new file mode 100755 index 00000000..b1d4604c --- /dev/null +++ b/bindings/rust/build_and_test.sh @@ -0,0 +1,212 @@ +#!/bin/bash + +# Rust bindings build and test script for ccap + +set -e + +detect_cli_devices() { + local cli_bin="" + local original_dir + original_dir="$(pwd)" + + # Prefer Debug build + if [ -x "$PROJECT_ROOT/build/Debug/ccap" ]; then + cli_bin="$PROJECT_ROOT/build/Debug/ccap" + elif [ -x "$PROJECT_ROOT/build/Debug/ccap.exe" ]; then + cli_bin="$PROJECT_ROOT/build/Debug/ccap.exe" + elif [ -x "$PROJECT_ROOT/build/Release/ccap" ]; then + cli_bin="$PROJECT_ROOT/build/Release/ccap" + elif [ -x "$PROJECT_ROOT/build/Release/ccap.exe" ]; then + cli_bin="$PROJECT_ROOT/build/Release/ccap.exe" + else + echo "ccap CLI not found. Building (Debug) with CCAP_BUILD_CLI=ON..." + pushd "$PROJECT_ROOT" >/dev/null + + mkdir -p build/Debug + pushd build/Debug >/dev/null + + # Reconfigure with CLI enabled (idempotent) + cmake ../.. -DCMAKE_BUILD_TYPE=Debug -DCCAP_BUILD_CLI=ON + cmake --build . --config Debug --target ccap-cli -- -j"$(nproc 2>/dev/null || echo 4)" + + popd >/dev/null + popd >/dev/null + + cli_bin="$PROJECT_ROOT/build/Debug/ccap" + fi + + cd "$original_dir" + echo "$cli_bin" +} + +parse_cli_device_count() { + local output="$1" + local count=0 + + if [[ "$output" =~ Found[[:space:]]+([0-9]+)[[:space:]]+camera ]]; then + count=${BASH_REMATCH[1]} + elif echo "$output" | grep -qi "No camera devices found"; then + count=0 + else + count=-1 + fi + + echo "$count" +} + +parse_rust_device_count() { + local output="$1" + local count=0 + + if [[ "$output" =~ \#\#[[:space:]]+Found[[:space:]]+([0-9]+)[[:space:]]+video[[:space:]]+capture[[:space:]]+device ]]; then + count=${BASH_REMATCH[1]} + elif echo "$output" | grep -qi "Failed to find any video capture device"; then + count=0 + else + count=-1 + fi + + echo "$count" +} + +echo "ccap Rust Bindings - Build and Test Script" +echo "===========================================" + +# Get the script directory and project root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +RUST_DIR="$SCRIPT_DIR" + +echo "Project root: $PROJECT_ROOT" +echo "Rust bindings: $RUST_DIR" + +# Build the C library first if needed +echo "" +echo "Step 1: Checking ccap C library..." +if [ ! -f "$PROJECT_ROOT/build/Debug/libccap.a" ] && [ ! -f "$PROJECT_ROOT/build/Release/libccap.a" ]; then + echo "ccap C library not found. Building..." + cd "$PROJECT_ROOT" + + mkdir -p build/Debug + cd build/Debug + + if [ ! -f "CMakeCache.txt" ]; then + cmake ../.. -DCMAKE_BUILD_TYPE=Debug + fi + + cmake --build . --config Debug -- -j"$(nproc 2>/dev/null || echo 4)" + + cd "$RUST_DIR" +else + echo "ccap C library found" +fi + +# Build Rust bindings +echo "" +echo "Step 2: Building Rust bindings..." +cd "$RUST_DIR" + +# Clean previous build +cargo clean + +# Build with default features +echo "Building with default features..." +cargo build + +# Build with async features +echo "Building with async features..." +cargo build --features async + +# Run tests +echo "" +echo "Step 3: Running tests..." +cargo test + +# Build examples +echo "" +echo "Step 4: Building examples..." +cargo build --examples +cargo build --features async --examples + +echo "" +echo "Step 5: Testing basic functionality (camera discovery vs CLI)..." + +# Run Rust discovery (print_camera) and capture output without aborting +set +e +RUST_DISCOVERY_OUTPUT=$(cargo run --example print_camera 2>&1) +RUST_DISCOVERY_STATUS=$? +set -e + +RUST_DEVICE_COUNT=$(parse_rust_device_count "$RUST_DISCOVERY_OUTPUT") + +CLI_BIN=$(detect_cli_devices) +if [ ! -x "$CLI_BIN" ]; then + echo "❌ Failed to build or locate ccap CLI for reference checks." >&2 + exit 1 +fi + +set +e +CLI_DISCOVERY_OUTPUT=$("$CLI_BIN" --list-devices 2>&1) +CLI_DISCOVERY_STATUS=$? +set -e + +CLI_DEVICE_COUNT=$(parse_cli_device_count "$CLI_DISCOVERY_OUTPUT") + +echo "Rust discovery exit: $RUST_DISCOVERY_STATUS, devices: $RUST_DEVICE_COUNT" +echo "CLI discovery exit: $CLI_DISCOVERY_STATUS, devices: $CLI_DEVICE_COUNT" + +# Decision logic +if [ $CLI_DISCOVERY_STATUS -ne 0 ]; then + echo "❌ CLI discovery failed. Output:" >&2 + echo "$CLI_DISCOVERY_OUTPUT" >&2 + exit 1 +fi + +if [ $CLI_DEVICE_COUNT -lt 0 ]; then + echo "❌ Unable to parse CLI device count. Output:" >&2 + echo "$CLI_DISCOVERY_OUTPUT" >&2 + exit 1 +fi + +if [ $RUST_DISCOVERY_STATUS -ne 0 ] && [ $CLI_DEVICE_COUNT -gt 0 ]; then + echo "❌ Rust discovery failed while CLI sees devices. Output:" >&2 + echo "$RUST_DISCOVERY_OUTPUT" >&2 + exit 1 +fi + +if [ $RUST_DEVICE_COUNT -lt 0 ]; then + echo "⚠️ Rust discovery output could not be parsed. Output:" >&2 + echo "$RUST_DISCOVERY_OUTPUT" >&2 + if [ $CLI_DEVICE_COUNT -gt 0 ]; then + echo "❌ CLI sees devices but Rust discovery is inconclusive." >&2 + exit 1 + fi +fi + +if [ $CLI_DEVICE_COUNT -eq 0 ] && [ $RUST_DEVICE_COUNT -eq 0 ]; then + echo "ℹ️ No cameras detected by CLI or Rust. Skipping capture tests (expected in headless environments)." +elif [ $CLI_DEVICE_COUNT -ne $RUST_DEVICE_COUNT ]; then + echo "❌ Device count mismatch (Rust: $RUST_DEVICE_COUNT, CLI: $CLI_DEVICE_COUNT)." >&2 + echo "Rust output:" >&2 + echo "$RUST_DISCOVERY_OUTPUT" >&2 + echo "CLI output:" >&2 + echo "$CLI_DISCOVERY_OUTPUT" >&2 + exit 1 +else + echo "✅ Camera discovery consistent (devices: $CLI_DEVICE_COUNT)." +fi + +echo "" +echo "✅ All Rust binding builds completed successfully!" +echo "" +echo "Usage examples:" +echo " cargo run --example print_camera" +echo " cargo run --example minimal_example" +echo " cargo run --example capture_grab" +echo " cargo run --example capture_callback" +echo " cargo run --features async --example capture_callback" +echo "" +echo "To use in your project, add to Cargo.toml:" +echo ' ccap = { path = "'$RUST_DIR'" }' +echo ' # or with async support:' +echo ' ccap = { path = "'$RUST_DIR'", features = ["async"] }' diff --git a/bindings/rust/examples/capture_callback.rs b/bindings/rust/examples/capture_callback.rs new file mode 100644 index 00000000..a90213ef --- /dev/null +++ b/bindings/rust/examples/capture_callback.rs @@ -0,0 +1,108 @@ +use ccap::{LogLevel, PixelFormat, PropertyName, Provider, Result, Utils}; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +fn main() -> Result<()> { + // Enable verbose log to see debug information + Utils::set_log_level(LogLevel::Verbose); + + // Set error callback to receive error notifications + Provider::set_error_callback(|error_code, description| { + eprintln!( + "Camera Error - Code: {}, Description: {}", + error_code, description + ); + }); + + let temp_provider = Provider::new()?; + let devices = temp_provider.list_devices()?; + if devices.is_empty() { + eprintln!("No camera devices found!"); + return Ok(()); + } + + for (i, device) in devices.iter().enumerate() { + println!("## Found video capture device: {}: {}", i, device); + } + + // Select camera device (automatically use first device for testing) + let device_index = 0; + + // Create provider with selected device + let mut provider = Provider::with_device(device_index)?; + + // Set camera properties + let requested_width = 1920; + let requested_height = 1080; + let requested_fps = 60.0; + + provider.set_property(PropertyName::Width, requested_width as f64)?; + provider.set_property(PropertyName::Height, requested_height as f64)?; + provider.set_property( + PropertyName::PixelFormatOutput, + PixelFormat::Bgra32 as u32 as f64, + )?; + provider.set_property(PropertyName::FrameRate, requested_fps)?; + + // Open and start camera + provider.open()?; + provider.start()?; + + if !provider.is_started() { + eprintln!("Failed to start camera!"); + return Ok(()); + } + + // Get real camera properties + let real_width = provider.get_property(PropertyName::Width)? as i32; + let real_height = provider.get_property(PropertyName::Height)? as i32; + let real_fps = provider.get_property(PropertyName::FrameRate)?; + + println!("Camera started successfully, requested resolution: {}x{}, real resolution: {}x{}, requested fps {}, real fps: {}", + requested_width, requested_height, real_width, real_height, requested_fps, real_fps); + + // Create directory for captures (using std::fs) + std::fs::create_dir_all("./image_capture") + .map_err(|e| ccap::CcapError::FileOperationFailed(e.to_string()))?; + + // Statistics tracking + let frame_count = Arc::new(Mutex::new(0u32)); + let frame_count_clone = frame_count.clone(); + + // Set frame callback + provider.set_new_frame_callback(move |frame| { + let mut count = frame_count_clone.lock().unwrap(); + *count += 1; + + println!( + "VideoFrame {} grabbed: width = {}, height = {}, bytes: {}", + frame.index(), + frame.width(), + frame.height(), + frame.data_size() + ); + + // Try to save frame to directory + if let Ok(filename) = Utils::dump_frame_to_directory(frame, "./image_capture") { + println!("VideoFrame saved to: {}", filename); + } else { + eprintln!("Failed to save frame!"); + } + + true // no need to retain the frame + })?; + + // Wait for 5 seconds to capture frames + println!("Capturing frames for 5 seconds..."); + thread::sleep(Duration::from_secs(5)); + + // Get final count + let final_count = *frame_count.lock().unwrap(); + println!("Captured {} frames, stopping...", final_count); + + // Remove callback before dropping + let _ = provider.remove_new_frame_callback(); + + Ok(()) +} diff --git a/bindings/rust/examples/capture_grab.rs b/bindings/rust/examples/capture_grab.rs new file mode 100644 index 00000000..a5ca5036 --- /dev/null +++ b/bindings/rust/examples/capture_grab.rs @@ -0,0 +1,73 @@ +use ccap::{LogLevel, PropertyName, Provider, Result, Utils}; +use std::fs; + +fn main() -> Result<()> { + // Enable verbose log to see debug information + Utils::set_log_level(LogLevel::Verbose); + + // Set error callback to receive error notifications + Provider::set_error_callback(|error_code, description| { + eprintln!( + "Camera Error - Code: {}, Description: {}", + error_code, description + ); + }); + + // Create a camera provider + let mut provider = Provider::new()?; + + // Open default device + provider.open()?; + provider.start_capture()?; + + if !provider.is_started() { + eprintln!("Failed to start camera!"); + return Ok(()); + } + + // Print the real resolution and fps after camera started + let real_width = provider.get_property(PropertyName::Width)? as u32; + let real_height = provider.get_property(PropertyName::Height)? as u32; + let real_fps = provider.get_property(PropertyName::FrameRate)?; + + println!( + "Camera started successfully, real resolution: {}x{}, real fps: {}", + real_width, real_height, real_fps + ); + + // Create capture directory + let capture_dir = "./image_capture"; + if !std::path::Path::new(capture_dir).exists() { + fs::create_dir_all(capture_dir).map_err(|e| { + ccap::CcapError::InvalidParameter(format!("Failed to create directory: {}", e)) + })?; + } + + // Capture frames (3000 ms timeout when grabbing frames) + let mut frame_count = 0; + while let Some(frame) = provider.grab_frame(3000)? { + let frame_info = frame.info()?; + println!( + "VideoFrame {} grabbed: width = {}, height = {}, bytes: {}", + frame_info.frame_index, frame_info.width, frame_info.height, frame_info.size_in_bytes + ); + + // Save frame to directory + match Utils::dump_frame_to_directory(&frame, capture_dir) { + Ok(dump_file) => { + println!("VideoFrame saved to: {}", dump_file); + } + Err(e) => { + eprintln!("Failed to save frame: {}", e); + } + } + + frame_count += 1; + if frame_count >= 10 { + println!("Captured 10 frames, stopping..."); + break; + } + } + + Ok(()) +} diff --git a/bindings/rust/examples/minimal_example.rs b/bindings/rust/examples/minimal_example.rs new file mode 100644 index 00000000..2fb73116 --- /dev/null +++ b/bindings/rust/examples/minimal_example.rs @@ -0,0 +1,55 @@ +use ccap::{Provider, Result, Utils}; + +fn main() -> Result<()> { + // Set error callback to receive error notifications + Provider::set_error_callback(|error_code, description| { + eprintln!( + "Error occurred - Code: {}, Description: {}", + error_code, description + ); + }); + + let temp_provider = Provider::new()?; + let devices = temp_provider.list_devices()?; + let camera_index = Utils::select_camera(&devices)?; + + // Use device index instead of name to avoid issues + let mut provider = Provider::with_device(camera_index as i32)?; + provider.open()?; + provider.start()?; + + if !provider.is_started() { + eprintln!("Failed to start camera!"); + return Ok(()); + } + + println!("Camera started successfully."); + + // Capture frames + for i in 0..10 { + match provider.grab_frame(3000) { + Ok(Some(frame)) => { + println!( + "VideoFrame {} grabbed: width = {}, height = {}, bytes: {}, format: {:?}", + frame.index(), + frame.width(), + frame.height(), + frame.data_size(), + frame.pixel_format() + ); + } + Ok(None) => { + eprintln!("Failed to grab frame {}!", i); + return Ok(()); + } + Err(e) => { + eprintln!("Error grabbing frame {}: {}", i, e); + return Ok(()); + } + } + } + + println!("Captured 10 frames, stopping..."); + let _ = provider.stop(); + Ok(()) +} diff --git a/bindings/rust/examples/print_camera.rs b/bindings/rust/examples/print_camera.rs new file mode 100644 index 00000000..d08dbaa2 --- /dev/null +++ b/bindings/rust/examples/print_camera.rs @@ -0,0 +1,83 @@ +use ccap::{LogLevel, Provider, Result, Utils}; + +fn find_camera_names() -> Result> { + // Create a temporary provider to query devices + let provider = Provider::new()?; + let devices = provider.list_devices()?; + + if !devices.is_empty() { + println!("## Found {} video capture device:", devices.len()); + for (index, name) in devices.iter().enumerate() { + println!(" {}: {}", index, name); + } + } else { + eprintln!("Failed to find any video capture device."); + } + + Ok(devices) +} + +fn print_camera_info(device_name: &str) -> Result<()> { + Utils::set_log_level(LogLevel::Verbose); + + // Create provider with specific device name + let provider = match Provider::with_device_name(device_name) { + Ok(p) => p, + Err(e) => { + eprintln!( + "### Failed to create provider for device: {}, error: {}", + device_name, e + ); + return Ok(()); + } + }; + + match provider.device_info() { + Ok(device_info) => { + println!("===== Info for device: {} =======", device_name); + + println!(" Supported resolutions:"); + for resolution in &device_info.supported_resolutions { + println!(" {}x{}", resolution.width, resolution.height); + } + + println!(" Supported pixel formats:"); + for format in &device_info.supported_pixel_formats { + println!(" {}", format.as_str()); + } + + println!("===== Info end =======\n"); + } + Err(e) => { + eprintln!( + "Failed to get device info for: {}, error: {}", + device_name, e + ); + } + } + + Ok(()) +} + +fn main() -> Result<()> { + // Set error callback to receive error notifications + Provider::set_error_callback(|error_code, description| { + eprintln!( + "Camera Error - Code: {}, Description: {}", + error_code, description + ); + }); + + let device_names = find_camera_names()?; + if device_names.is_empty() { + return Ok(()); + } + + for name in &device_names { + if let Err(e) = print_camera_info(name) { + eprintln!("Error processing device {}: {}", name, e); + } + } + + Ok(()) +} diff --git a/bindings/rust/src/async.rs b/bindings/rust/src/async.rs new file mode 100644 index 00000000..56f090b9 --- /dev/null +++ b/bindings/rust/src/async.rs @@ -0,0 +1,134 @@ +//! Async support for ccap +//! +//! This module provides async/await interfaces for camera capture operations. + +#[cfg(feature = "async")] +use crate::{Provider as SyncProvider, Result, VideoFrame}; +#[cfg(feature = "async")] +use std::sync::Arc; +#[cfg(feature = "async")] +use std::time::Duration; +#[cfg(feature = "async")] +use tokio::sync::{mpsc, Mutex}; + +#[cfg(feature = "async")] +/// Async camera provider wrapper +/// +/// Note: The frame streaming feature is incomplete. The frame_sender is reserved +/// for future implementation where frames will be automatically pushed to the stream. +pub struct AsyncProvider { + provider: Arc>, + frame_receiver: Option>, + /// Kept for future frame streaming implementation + #[allow(dead_code)] + _frame_sender: mpsc::UnboundedSender, +} + +#[cfg(feature = "async")] +impl AsyncProvider { + /// Create a new async provider + pub async fn new() -> Result { + let provider = SyncProvider::new()?; + let (tx, rx) = mpsc::unbounded_channel(); + + Ok(AsyncProvider { + provider: Arc::new(Mutex::new(provider)), + frame_receiver: Some(rx), + _frame_sender: tx, + }) + } + + /// Find available camera devices + pub async fn find_device_names(&self) -> Result> { + let provider = self.provider.lock().await; + provider.find_device_names() + } + + /// Open a camera device + pub async fn open(&self, device_name: Option<&str>, auto_start: bool) -> Result<()> { + let mut provider = self.provider.lock().await; + provider.open_device(device_name, auto_start) + } + + /// Start capturing frames + pub async fn start(&self) -> Result<()> { + let mut provider = self.provider.lock().await; + provider.start() + } + + /// Stop capturing frames + pub async fn stop(&self) -> Result<()> { + let mut provider = self.provider.lock().await; + provider.stop() + } + + /// Check if the camera is opened + pub async fn is_opened(&self) -> bool { + let provider = self.provider.lock().await; + provider.is_opened() + } + + /// Check if capture is started + pub async fn is_started(&self) -> bool { + let provider = self.provider.lock().await; + provider.is_started() + } + + /// Grab a frame asynchronously with timeout + pub async fn grab_frame_timeout(&self, timeout: Duration) -> Result> { + let provider = Arc::clone(&self.provider); + let timeout_ms = timeout.as_millis() as u32; + + tokio::task::spawn_blocking(move || { + let mut provider = provider.blocking_lock(); + provider.grab_frame(timeout_ms) + }) + .await + .map_err(|e| crate::CcapError::InternalError(e.to_string()))? + } + + /// Grab a frame asynchronously (non-blocking) + pub async fn grab_frame(&self) -> Result> { + self.grab_frame_timeout(Duration::from_millis(0)).await + } + + #[cfg(feature = "async")] + /// Create a stream of frames + pub fn frame_stream(&mut self) -> impl futures::Stream { + let receiver = self + .frame_receiver + .take() + .expect("Frame stream can only be created once"); + + tokio_stream::wrappers::UnboundedReceiverStream::new(receiver) + } +} + +#[cfg(feature = "async")] +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_async_provider_creation() { + let provider = AsyncProvider::new().await; + assert!(provider.is_ok()); + } + + #[tokio::test] + async fn test_async_find_devices() { + let provider = AsyncProvider::new() + .await + .expect("Failed to create provider"); + let devices = provider.find_device_names().await; + + match devices { + Ok(devices) => { + println!("Found {} devices:", devices.len()); + } + Err(e) => { + println!("No devices found or error: {:?}", e); + } + } + } +} diff --git a/bindings/rust/src/convert.rs b/bindings/rust/src/convert.rs new file mode 100644 index 00000000..ba6cc8e9 --- /dev/null +++ b/bindings/rust/src/convert.rs @@ -0,0 +1,273 @@ +use crate::error::{CcapError, Result}; +use crate::sys; +use crate::types::ColorConversionBackend; +use std::os::raw::c_int; + +/// Color conversion utilities +pub struct Convert; + +impl Convert { + /// Get current color conversion backend + pub fn backend() -> ColorConversionBackend { + let backend = unsafe { sys::ccap_convert_get_backend() }; + ColorConversionBackend::from_c_enum(backend) + } + + /// Set color conversion backend + pub fn set_backend(backend: ColorConversionBackend) -> Result<()> { + let success = unsafe { sys::ccap_convert_set_backend(backend.to_c_enum()) }; + + if success { + Ok(()) + } else { + Err(CcapError::BackendSetFailed) + } + } + + /// Check if AVX2 is available + pub fn has_avx2() -> bool { + unsafe { sys::ccap_convert_has_avx2() } + } + + /// Check if Apple Accelerate is available + pub fn has_apple_accelerate() -> bool { + unsafe { sys::ccap_convert_has_apple_accelerate() } + } + + /// Check if NEON is available + pub fn has_neon() -> bool { + unsafe { sys::ccap_convert_has_neon() } + } + + /// Convert YUYV to RGB24 + pub fn yuyv_to_rgb24( + src_data: &[u8], + src_stride: usize, + width: u32, + height: u32, + ) -> Result> { + let dst_stride = (width * 3) as usize; + let dst_size = dst_stride * height as usize; + let mut dst_data = vec![0u8; dst_size]; + + unsafe { + sys::ccap_convert_yuyv_to_rgb24( + src_data.as_ptr(), + src_stride as c_int, + dst_data.as_mut_ptr(), + dst_stride as c_int, + width as c_int, + height as c_int, + sys::CcapConvertFlag_CCAP_CONVERT_FLAG_DEFAULT, + ) + }; + + Ok(dst_data) + } + + /// Convert YUYV to BGR24 + pub fn yuyv_to_bgr24( + src_data: &[u8], + src_stride: usize, + width: u32, + height: u32, + ) -> Result> { + let dst_stride = (width * 3) as usize; + let dst_size = dst_stride * height as usize; + let mut dst_data = vec![0u8; dst_size]; + + unsafe { + sys::ccap_convert_yuyv_to_bgr24( + src_data.as_ptr(), + src_stride as c_int, + dst_data.as_mut_ptr(), + dst_stride as c_int, + width as c_int, + height as c_int, + sys::CcapConvertFlag_CCAP_CONVERT_FLAG_DEFAULT, + ) + }; + + Ok(dst_data) + } + + /// Convert RGB to BGR + pub fn rgb_to_bgr( + src_data: &[u8], + src_stride: usize, + width: u32, + height: u32, + ) -> Result> { + let dst_stride = (width * 3) as usize; + let dst_size = dst_stride * height as usize; + let mut dst_data = vec![0u8; dst_size]; + + unsafe { + sys::ccap_convert_rgb_to_bgr( + src_data.as_ptr(), + src_stride as c_int, + dst_data.as_mut_ptr(), + dst_stride as c_int, + width as c_int, + height as c_int, + ) + }; + + Ok(dst_data) + } + + /// Convert BGR to RGB + pub fn bgr_to_rgb( + src_data: &[u8], + src_stride: usize, + width: u32, + height: u32, + ) -> Result> { + let dst_stride = (width * 3) as usize; + let dst_size = dst_stride * height as usize; + let mut dst_data = vec![0u8; dst_size]; + + unsafe { + sys::ccap_convert_bgr_to_rgb( + src_data.as_ptr(), + src_stride as c_int, + dst_data.as_mut_ptr(), + dst_stride as c_int, + width as c_int, + height as c_int, + ) + }; + + Ok(dst_data) + } + + /// Convert NV12 to RGB24 + pub fn nv12_to_rgb24( + y_data: &[u8], + y_stride: usize, + uv_data: &[u8], + uv_stride: usize, + width: u32, + height: u32, + ) -> Result> { + let dst_stride = (width * 3) as usize; + let dst_size = dst_stride * height as usize; + let mut dst_data = vec![0u8; dst_size]; + + unsafe { + sys::ccap_convert_nv12_to_rgb24( + y_data.as_ptr(), + y_stride as c_int, + uv_data.as_ptr(), + uv_stride as c_int, + dst_data.as_mut_ptr(), + dst_stride as c_int, + width as c_int, + height as c_int, + sys::CcapConvertFlag_CCAP_CONVERT_FLAG_DEFAULT, + ) + }; + + Ok(dst_data) + } + + /// Convert NV12 to BGR24 + pub fn nv12_to_bgr24( + y_data: &[u8], + y_stride: usize, + uv_data: &[u8], + uv_stride: usize, + width: u32, + height: u32, + ) -> Result> { + let dst_stride = (width * 3) as usize; + let dst_size = dst_stride * height as usize; + let mut dst_data = vec![0u8; dst_size]; + + unsafe { + sys::ccap_convert_nv12_to_bgr24( + y_data.as_ptr(), + y_stride as c_int, + uv_data.as_ptr(), + uv_stride as c_int, + dst_data.as_mut_ptr(), + dst_stride as c_int, + width as c_int, + height as c_int, + sys::CcapConvertFlag_CCAP_CONVERT_FLAG_DEFAULT, + ) + }; + + Ok(dst_data) + } + + /// Convert I420 to RGB24 + #[allow(clippy::too_many_arguments)] + pub fn i420_to_rgb24( + y_data: &[u8], + y_stride: usize, + u_data: &[u8], + u_stride: usize, + v_data: &[u8], + v_stride: usize, + width: u32, + height: u32, + ) -> Result> { + let dst_stride = (width * 3) as usize; + let dst_size = dst_stride * height as usize; + let mut dst_data = vec![0u8; dst_size]; + + unsafe { + sys::ccap_convert_i420_to_rgb24( + y_data.as_ptr(), + y_stride as c_int, + u_data.as_ptr(), + u_stride as c_int, + v_data.as_ptr(), + v_stride as c_int, + dst_data.as_mut_ptr(), + dst_stride as c_int, + width as c_int, + height as c_int, + sys::CcapConvertFlag_CCAP_CONVERT_FLAG_DEFAULT, + ) + }; + + Ok(dst_data) + } + + /// Convert I420 to BGR24 + #[allow(clippy::too_many_arguments)] + pub fn i420_to_bgr24( + y_data: &[u8], + y_stride: usize, + u_data: &[u8], + u_stride: usize, + v_data: &[u8], + v_stride: usize, + width: u32, + height: u32, + ) -> Result> { + let dst_stride = (width * 3) as usize; + let dst_size = dst_stride * height as usize; + let mut dst_data = vec![0u8; dst_size]; + + unsafe { + sys::ccap_convert_i420_to_bgr24( + y_data.as_ptr(), + y_stride as c_int, + u_data.as_ptr(), + u_stride as c_int, + v_data.as_ptr(), + v_stride as c_int, + dst_data.as_mut_ptr(), + dst_stride as c_int, + width as c_int, + height as c_int, + sys::CcapConvertFlag_CCAP_CONVERT_FLAG_DEFAULT, + ) + }; + + Ok(dst_data) + } +} diff --git a/bindings/rust/src/error.rs b/bindings/rust/src/error.rs new file mode 100644 index 00000000..0793f65f --- /dev/null +++ b/bindings/rust/src/error.rs @@ -0,0 +1,122 @@ +//! Error handling for ccap library + +/// Error types for ccap operations +#[derive(Debug)] +pub enum CcapError { + /// No error occurred + None, + + /// No camera device found + NoDeviceFound, + + /// Invalid device specified + InvalidDevice(String), + + /// Camera device open failed + DeviceOpenFailed, + + /// Device already opened + DeviceAlreadyOpened, + + /// Device not opened + DeviceNotOpened, + + /// Capture start failed + CaptureStartFailed, + + /// Capture stop failed + CaptureStopFailed, + + /// Frame grab failed + FrameGrabFailed, + + /// Timeout occurred + Timeout, + + /// Invalid parameter + InvalidParameter(String), + + /// Not supported operation + NotSupported, + + /// Backend set failed + BackendSetFailed, + + /// String conversion error + StringConversionError(String), + + /// File operation failed + FileOperationFailed(String), + + /// Device not found (alias for NoDeviceFound for compatibility) + DeviceNotFound, + + /// Internal error + InternalError(String), + + /// Unknown error with error code + Unknown { + /// Error code from the underlying system + code: i32, + }, +} + +impl std::fmt::Display for CcapError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CcapError::None => write!(f, "No error"), + CcapError::NoDeviceFound => write!(f, "No camera device found"), + CcapError::InvalidDevice(name) => write!(f, "Invalid device: {}", name), + CcapError::DeviceOpenFailed => write!(f, "Camera device open failed"), + CcapError::DeviceAlreadyOpened => write!(f, "Device already opened"), + CcapError::DeviceNotOpened => write!(f, "Device not opened"), + CcapError::CaptureStartFailed => write!(f, "Capture start failed"), + CcapError::CaptureStopFailed => write!(f, "Capture stop failed"), + CcapError::FrameGrabFailed => write!(f, "Frame grab failed"), + CcapError::Timeout => write!(f, "Timeout occurred"), + CcapError::InvalidParameter(param) => write!(f, "Invalid parameter: {}", param), + CcapError::NotSupported => write!(f, "Operation not supported"), + CcapError::BackendSetFailed => write!(f, "Backend set failed"), + CcapError::StringConversionError(msg) => write!(f, "String conversion error: {}", msg), + CcapError::FileOperationFailed(msg) => write!(f, "File operation failed: {}", msg), + CcapError::DeviceNotFound => write!(f, "Device not found"), + CcapError::InternalError(msg) => write!(f, "Internal error: {}", msg), + CcapError::Unknown { code } => write!(f, "Unknown error: {}", code), + } + } +} + +impl std::error::Error for CcapError {} + +impl From for CcapError { + fn from(code: i32) -> Self { + use crate::sys::*; + + // Convert i32 to CcapErrorCode for matching + // On some platforms CcapErrorCode might be unsigned + let code_u = code as CcapErrorCode; + + #[allow(non_upper_case_globals)] + match code_u { + CcapErrorCode_CCAP_ERROR_NONE => CcapError::None, + CcapErrorCode_CCAP_ERROR_NO_DEVICE_FOUND => CcapError::NoDeviceFound, + CcapErrorCode_CCAP_ERROR_INVALID_DEVICE => CcapError::InvalidDevice("".to_string()), + CcapErrorCode_CCAP_ERROR_DEVICE_OPEN_FAILED => CcapError::DeviceOpenFailed, + CcapErrorCode_CCAP_ERROR_DEVICE_START_FAILED => CcapError::CaptureStartFailed, + CcapErrorCode_CCAP_ERROR_DEVICE_STOP_FAILED => CcapError::CaptureStopFailed, + CcapErrorCode_CCAP_ERROR_FRAME_CAPTURE_FAILED => CcapError::FrameGrabFailed, + CcapErrorCode_CCAP_ERROR_FRAME_CAPTURE_TIMEOUT => CcapError::Timeout, + CcapErrorCode_CCAP_ERROR_UNSUPPORTED_PIXEL_FORMAT => CcapError::NotSupported, + CcapErrorCode_CCAP_ERROR_UNSUPPORTED_RESOLUTION => CcapError::NotSupported, + CcapErrorCode_CCAP_ERROR_PROPERTY_SET_FAILED => { + CcapError::InvalidParameter("".to_string()) + } + CcapErrorCode_CCAP_ERROR_MEMORY_ALLOCATION_FAILED => CcapError::Unknown { code }, + CcapErrorCode_CCAP_ERROR_INTERNAL_ERROR => CcapError::Unknown { code }, + _ => CcapError::Unknown { code }, + } + } +} + +/// Result type for ccap operations +pub type Result = std::result::Result; diff --git a/bindings/rust/src/frame.rs b/bindings/rust/src/frame.rs new file mode 100644 index 00000000..5d73f625 --- /dev/null +++ b/bindings/rust/src/frame.rs @@ -0,0 +1,216 @@ +use crate::{error::CcapError, sys, types::*}; +use std::ffi::CStr; + +/// Device information structure +#[derive(Debug, Clone)] +pub struct DeviceInfo { + /// Device name + pub name: String, + /// Supported pixel formats + pub supported_pixel_formats: Vec, + /// Supported resolutions + pub supported_resolutions: Vec, +} + +impl DeviceInfo { + /// Create DeviceInfo from C structure + pub fn from_c_struct(info: &sys::CcapDeviceInfo) -> Result { + let name_cstr = unsafe { CStr::from_ptr(info.deviceName.as_ptr()) }; + let name = name_cstr + .to_str() + .map_err(|e| CcapError::StringConversionError(e.to_string()))? + .to_string(); + + // Ensure we don't exceed array bounds + let format_count = (info.pixelFormatCount).min(info.supportedPixelFormats.len()); + let supported_pixel_formats = info.supportedPixelFormats[..format_count] + .iter() + .map(|&format| PixelFormat::from_c_enum(format)) + .collect(); + + let resolution_count = (info.resolutionCount).min(info.supportedResolutions.len()); + let supported_resolutions = info.supportedResolutions[..resolution_count] + .iter() + .map(|&res| Resolution::from(res)) + .collect(); + + Ok(DeviceInfo { + name, + supported_pixel_formats, + supported_resolutions, + }) + } +} + +/// Video frame wrapper +pub struct VideoFrame { + frame: *mut sys::CcapVideoFrame, + owns_frame: bool, // Whether we own the frame and should release it +} + +impl VideoFrame { + pub(crate) fn from_c_ptr(frame: *mut sys::CcapVideoFrame) -> Self { + VideoFrame { + frame, + owns_frame: true, + } + } + + /// Create frame from raw pointer without owning it (for callbacks) + pub(crate) fn from_c_ptr_ref(frame: *mut sys::CcapVideoFrame) -> Self { + VideoFrame { + frame, + owns_frame: false, + } + } + + /// Get the internal C pointer (for internal use) + #[allow(dead_code)] + pub(crate) fn as_c_ptr(&self) -> *const sys::CcapVideoFrame { + self.frame as *const sys::CcapVideoFrame + } + + /// Create frame from raw pointer (for internal use) + #[allow(dead_code)] + pub(crate) fn from_raw(frame: *mut sys::CcapVideoFrame) -> Option { + if frame.is_null() { + None + } else { + Some(VideoFrame { + frame, + owns_frame: true, + }) + } + } + + /// Get frame information + pub fn info<'a>(&'a self) -> crate::error::Result> { + let mut info = sys::CcapVideoFrameInfo::default(); + + let success = unsafe { sys::ccap_video_frame_get_info(self.frame, &mut info) }; + + if success { + // Calculate proper plane sizes based on pixel format + // For plane 0 (Y or main): stride * height + // For chroma planes (UV): stride * height/2 for most formats + let plane0_size = (info.stride[0] as usize) * (info.height as usize); + let plane1_size = if info.stride[1] > 0 { + (info.stride[1] as usize) * ((info.height as usize + 1) / 2) + } else { + 0 + }; + let plane2_size = if info.stride[2] > 0 { + (info.stride[2] as usize) * ((info.height as usize + 1) / 2) + } else { + 0 + }; + + Ok(VideoFrameInfo { + width: info.width, + height: info.height, + pixel_format: PixelFormat::from(info.pixelFormat), + size_in_bytes: info.sizeInBytes, + timestamp: info.timestamp, + frame_index: info.frameIndex, + orientation: FrameOrientation::from(info.orientation), + data_planes: [ + if info.data[0].is_null() { + None + } else { + Some(unsafe { std::slice::from_raw_parts(info.data[0], plane0_size) }) + }, + if info.data[1].is_null() { + None + } else { + Some(unsafe { std::slice::from_raw_parts(info.data[1], plane1_size) }) + }, + if info.data[2].is_null() { + None + } else { + Some(unsafe { std::slice::from_raw_parts(info.data[2], plane2_size) }) + }, + ], + strides: [info.stride[0], info.stride[1], info.stride[2]], + }) + } else { + Err(CcapError::FrameGrabFailed) + } + } + + /// Get all frame data as a slice + pub fn data(&self) -> crate::error::Result<&[u8]> { + let mut info = sys::CcapVideoFrameInfo::default(); + + let success = unsafe { sys::ccap_video_frame_get_info(self.frame, &mut info) }; + + if success && !info.data[0].is_null() { + Ok(unsafe { std::slice::from_raw_parts(info.data[0], info.sizeInBytes as usize) }) + } else { + Err(CcapError::FrameGrabFailed) + } + } + + /// Get frame width (convenience method) + pub fn width(&self) -> u32 { + self.info().map(|info| info.width).unwrap_or(0) + } + + /// Get frame height (convenience method) + pub fn height(&self) -> u32 { + self.info().map(|info| info.height).unwrap_or(0) + } + + /// Get pixel format (convenience method) + pub fn pixel_format(&self) -> PixelFormat { + self.info() + .map(|info| info.pixel_format) + .unwrap_or(PixelFormat::Unknown) + } + + /// Get data size in bytes (convenience method) + pub fn data_size(&self) -> u32 { + self.info().map(|info| info.size_in_bytes).unwrap_or(0) + } + + /// Get frame index (convenience method) + pub fn index(&self) -> u64 { + self.info().map(|info| info.frame_index).unwrap_or(0) + } +} + +impl Drop for VideoFrame { + fn drop(&mut self) { + if self.owns_frame { + unsafe { + sys::ccap_video_frame_release(self.frame); + } + } + } +} + +// Make VideoFrame Send + Sync if the underlying C library supports it +unsafe impl Send for VideoFrame {} +unsafe impl Sync for VideoFrame {} + +/// High-level video frame information +#[derive(Debug)] +pub struct VideoFrameInfo<'a> { + /// Frame width in pixels + pub width: u32, + /// Frame height in pixels + pub height: u32, + /// Pixel format of the frame + pub pixel_format: PixelFormat, + /// Size of frame data in bytes + pub size_in_bytes: u32, + /// Frame timestamp + pub timestamp: u64, + /// Frame sequence index + pub frame_index: u64, + /// Frame orientation + pub orientation: FrameOrientation, + /// Frame data planes (up to 3 planes) + pub data_planes: [Option<&'a [u8]>; 3], + /// Stride values for each plane + pub strides: [u32; 3], +} diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs new file mode 100644 index 00000000..bd151e3c --- /dev/null +++ b/bindings/rust/src/lib.rs @@ -0,0 +1,40 @@ +//! # ccap - Cross-platform Camera Capture Library +//! +//! A high-performance, lightweight camera capture library with Rust bindings. + +#![warn(missing_docs)] +#![warn(rust_2018_idioms)] + +// Re-export the low-level bindings for advanced users +/// Low-level FFI bindings to ccap C library +pub mod sys { + #![allow(non_upper_case_globals)] + #![allow(non_camel_case_types)] + #![allow(non_snake_case)] + #![allow(dead_code)] + #![allow(missing_docs)] + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} + +mod convert; +mod error; +mod frame; +mod provider; +mod types; +mod utils; + +#[cfg(feature = "async")] +pub mod r#async; + +// Public re-exports +pub use convert::Convert; +pub use error::{CcapError, Result}; +pub use frame::*; +pub use provider::Provider; +pub use types::*; +pub use utils::{LogLevel, Utils}; + +/// Get library version string +pub fn version() -> Result { + Provider::version() +} diff --git a/bindings/rust/src/provider.rs b/bindings/rust/src/provider.rs new file mode 100644 index 00000000..42a204e7 --- /dev/null +++ b/bindings/rust/src/provider.rs @@ -0,0 +1,500 @@ +//! Camera provider for synchronous camera capture operations + +use crate::{error::*, frame::*, sys, types::*}; +use std::ffi::{CStr, CString}; +use std::ptr; + +/// Camera provider for synchronous camera capture operations +pub struct Provider { + handle: *mut sys::CcapProvider, + is_opened: bool, + callback_ptr: Option<*mut std::ffi::c_void>, +} + +unsafe impl Send for Provider {} + +impl Provider { + /// Create a new camera provider + pub fn new() -> Result { + let handle = unsafe { sys::ccap_provider_create() }; + if handle.is_null() { + return Err(CcapError::DeviceOpenFailed); + } + + Ok(Provider { + handle, + is_opened: false, + callback_ptr: None, + }) + } + + /// Create a provider with a specific device index + pub fn with_device(device_index: i32) -> Result { + let handle = unsafe { sys::ccap_provider_create_with_index(device_index, ptr::null()) }; + if handle.is_null() { + return Err(CcapError::InvalidDevice(format!( + "device index {}", + device_index + ))); + } + + Ok(Provider { + handle, + is_opened: true, // C API likely opens device automatically + callback_ptr: None, + }) + } + + /// Create a provider with a specific device name + pub fn with_device_name>(device_name: S) -> Result { + let c_name = CString::new(device_name.as_ref()).map_err(|_| { + CcapError::InvalidParameter("device name contains null byte".to_string()) + })?; + + let handle = unsafe { sys::ccap_provider_create_with_device(c_name.as_ptr(), ptr::null()) }; + if handle.is_null() { + return Err(CcapError::InvalidDevice(device_name.as_ref().to_string())); + } + + Ok(Provider { + handle, + is_opened: true, // C API likely opens device automatically + callback_ptr: None, + }) + } + + /// Get available camera devices + pub fn get_devices() -> Result> { + // Create a temporary provider to query devices + let provider = Self::new()?; + let mut device_names_list = sys::CcapDeviceNamesList::default(); + + let success = unsafe { + sys::ccap_provider_find_device_names_list(provider.handle, &mut device_names_list) + }; + + if !success { + return Ok(Vec::new()); + } + + let mut devices = Vec::new(); + for i in 0..device_names_list.deviceCount { + let name_bytes = &device_names_list.deviceNames[i]; + let name = unsafe { + let cstr = CStr::from_ptr(name_bytes.as_ptr()); + cstr.to_string_lossy().to_string() + }; + + // Try to get device info by creating provider with this device + if let Ok(device_provider) = Self::with_device_name(&name) { + if let Ok(device_info) = device_provider.get_device_info_direct() { + devices.push(device_info); + } else { + // Fallback: create minimal device info from just the name + devices.push(DeviceInfo { + name, + supported_pixel_formats: Vec::new(), + supported_resolutions: Vec::new(), + }); + } + } + } + + Ok(devices) + } + + /// Get device info directly from current provider + fn get_device_info_direct(&self) -> Result { + let mut device_info = sys::CcapDeviceInfo::default(); + + let success = unsafe { sys::ccap_provider_get_device_info(self.handle, &mut device_info) }; + + if !success { + return Err(CcapError::DeviceOpenFailed); + } + + let name = unsafe { + let cstr = CStr::from_ptr(device_info.deviceName.as_ptr()); + cstr.to_string_lossy().to_string() + }; + + let mut formats = Vec::new(); + for i in 0..device_info.pixelFormatCount { + if i < device_info.supportedPixelFormats.len() { + formats.push(PixelFormat::from(device_info.supportedPixelFormats[i])); + } + } + + let mut resolutions = Vec::new(); + for i in 0..device_info.resolutionCount { + if i < device_info.supportedResolutions.len() { + let res = &device_info.supportedResolutions[i]; + resolutions.push(Resolution { + width: res.width, + height: res.height, + }); + } + } + + Ok(DeviceInfo { + name, + supported_pixel_formats: formats, + supported_resolutions: resolutions, + }) + } + + /// Open the camera device + pub fn open(&mut self) -> Result<()> { + if self.is_opened { + return Ok(()); + } + + let result = unsafe { sys::ccap_provider_open_by_index(self.handle, -1, false) }; + if !result { + return Err(CcapError::DeviceOpenFailed); + } + + self.is_opened = true; + Ok(()) + } + + /// Open device with optional device name and auto start + pub fn open_device(&mut self, device_name: Option<&str>, auto_start: bool) -> Result<()> { + if let Some(name) = device_name { + // Recreate provider with specific device + if !self.handle.is_null() { + unsafe { + sys::ccap_provider_destroy(self.handle); + } + } + let c_name = CString::new(name).map_err(|_| { + CcapError::InvalidParameter("device name contains null byte".to_string()) + })?; + self.handle = + unsafe { sys::ccap_provider_create_with_device(c_name.as_ptr(), ptr::null()) }; + if self.handle.is_null() { + return Err(CcapError::InvalidDevice(name.to_string())); + } + self.is_opened = true; + } else { + self.open()?; + } + if auto_start { + self.start_capture()?; + } + Ok(()) + } + + /// Get device info for the current provider + pub fn device_info(&self) -> Result { + self.get_device_info_direct() + } + + /// Check if capture is started + pub fn is_started(&self) -> bool { + unsafe { sys::ccap_provider_is_started(self.handle) } + } + + /// Start capture (alias for start_capture) + pub fn start(&mut self) -> Result<()> { + self.start_capture() + } + + /// Stop capture (alias for stop_capture) + pub fn stop(&mut self) -> Result<()> { + self.stop_capture() + } + + /// Check if the camera is opened + pub fn is_opened(&self) -> bool { + self.is_opened + } + + /// Set camera property + pub fn set_property(&mut self, property: PropertyName, value: f64) -> Result<()> { + let property_id: sys::CcapPropertyName = property.into(); + let success = unsafe { sys::ccap_provider_set_property(self.handle, property_id, value) }; + + if !success { + return Err(CcapError::InvalidParameter(format!( + "property {:?}", + property + ))); + } + + Ok(()) + } + + /// Get camera property + pub fn get_property(&self, property: PropertyName) -> Result { + let property_id: sys::CcapPropertyName = property.into(); + let value = unsafe { sys::ccap_provider_get_property(self.handle, property_id) }; + + Ok(value) + } + + /// Set camera resolution + pub fn set_resolution(&mut self, width: u32, height: u32) -> Result<()> { + self.set_property(PropertyName::Width, width as f64)?; + self.set_property(PropertyName::Height, height as f64)?; + Ok(()) + } + + /// Set camera frame rate + pub fn set_frame_rate(&mut self, fps: f64) -> Result<()> { + self.set_property(PropertyName::FrameRate, fps) + } + + /// Set pixel format + pub fn set_pixel_format(&mut self, format: PixelFormat) -> Result<()> { + self.set_property(PropertyName::PixelFormatOutput, format.to_c_enum() as f64) + } + + /// Grab a single frame with timeout + pub fn grab_frame(&mut self, timeout_ms: u32) -> Result> { + if !self.is_opened { + return Err(CcapError::DeviceNotOpened); + } + + let frame = unsafe { sys::ccap_provider_grab(self.handle, timeout_ms) }; + if frame.is_null() { + return Ok(None); + } + + Ok(Some(VideoFrame::from_c_ptr(frame))) + } + + /// Start continuous capture + pub fn start_capture(&mut self) -> Result<()> { + if !self.is_opened { + return Err(CcapError::DeviceNotOpened); + } + + let result = unsafe { sys::ccap_provider_start(self.handle) }; + if !result { + return Err(CcapError::CaptureStartFailed); + } + + Ok(()) + } + + /// Stop continuous capture + pub fn stop_capture(&mut self) -> Result<()> { + unsafe { sys::ccap_provider_stop(self.handle) }; + Ok(()) + } + + /// Get library version + pub fn version() -> Result { + let version_ptr = unsafe { sys::ccap_get_version() }; + if version_ptr.is_null() { + return Err(CcapError::Unknown { code: -1 }); + } + + let version_cstr = unsafe { CStr::from_ptr(version_ptr) }; + version_cstr + .to_str() + .map(|s| s.to_string()) + .map_err(|_| CcapError::Unknown { code: -2 }) + } + + /// List device names (simple string list) + pub fn list_devices(&self) -> Result> { + let device_infos = Self::get_devices()?; + Ok(device_infos.into_iter().map(|info| info.name).collect()) + } + + /// Find device names (alias for list_devices) + pub fn find_device_names(&self) -> Result> { + self.list_devices() + } + + /// Get current resolution (convenience getter) + pub fn resolution(&self) -> Result<(u32, u32)> { + let width = self.get_property(PropertyName::Width)? as u32; + let height = self.get_property(PropertyName::Height)? as u32; + Ok((width, height)) + } + + /// Get current pixel format (convenience getter) + pub fn pixel_format(&self) -> Result { + let format_val = self.get_property(PropertyName::PixelFormatOutput)? as u32; + Ok(PixelFormat::from_c_enum(format_val as sys::CcapPixelFormat)) + } + + /// Get current frame rate (convenience getter) + pub fn frame_rate(&self) -> Result { + self.get_property(PropertyName::FrameRate) + } + + /// Set error callback for camera errors + /// + /// # Memory Safety + /// + /// This is a **global** callback that persists for the lifetime of the program. + /// The callback memory is intentionally leaked as it's meant to be set once + /// and used throughout the application lifetime. + /// + /// If you need to change or remove the callback, consider using instance-level + /// callbacks via `set_new_frame_callback` instead. + pub fn set_error_callback(callback: F) + where + F: Fn(i32, &str) + Send + Sync + 'static, + { + use std::os::raw::c_char; + use std::sync::{Arc, Mutex}; + + let callback = Arc::new(Mutex::new(callback)); + + unsafe extern "C" fn error_callback_wrapper( + error_code: sys::CcapErrorCode, + description: *const c_char, + user_data: *mut std::ffi::c_void, + ) { + if user_data.is_null() || description.is_null() { + return; + } + + let callback = &*(user_data as *const Arc>); + let desc_cstr = std::ffi::CStr::from_ptr(description); + if let Ok(desc_str) = desc_cstr.to_str() { + if let Ok(cb) = callback.lock() { + cb(error_code as i32, desc_str); + } + } + } + + // Store the callback to prevent it from being dropped + let callback_ptr = Box::into_raw(Box::new(callback)); + + unsafe { + sys::ccap_set_error_callback( + Some(error_callback_wrapper), + callback_ptr as *mut std::ffi::c_void, + ); + } + + // Note: This leaks memory, but it's acceptable for a global callback + // In a production system, you'd want to provide a way to unregister callbacks + } + + /// Open device with index and auto start + pub fn open_with_index(&mut self, device_index: i32, auto_start: bool) -> Result<()> { + // Destroy old handle if exists + if !self.handle.is_null() { + unsafe { + sys::ccap_provider_destroy(self.handle); + } + } + + // Clean up old callback if exists + self.cleanup_callback(); + + // Create a new provider with the specified device index + self.handle = unsafe { sys::ccap_provider_create_with_index(device_index, ptr::null()) }; + + if self.handle.is_null() { + return Err(CcapError::InvalidDevice(format!( + "device index {}", + device_index + ))); + } + + self.is_opened = false; + self.open()?; + if auto_start { + self.start_capture()?; + } + Ok(()) + } + + /// Set a callback for new frame notifications + pub fn set_new_frame_callback(&mut self, callback: F) -> Result<()> + where + F: Fn(&VideoFrame) -> bool + Send + Sync + 'static, + { + use std::os::raw::c_void; + + // Clean up old callback if exists + self.cleanup_callback(); + + unsafe extern "C" fn new_frame_callback_wrapper( + frame: *const sys::CcapVideoFrame, + user_data: *mut c_void, + ) -> bool { + if user_data.is_null() || frame.is_null() { + return false; + } + + let callback = &*(user_data as *const Box bool + Send + Sync>); + + // Create a temporary VideoFrame wrapper that doesn't own the frame + let video_frame = VideoFrame::from_c_ptr_ref(frame as *mut sys::CcapVideoFrame); + callback(&video_frame) + } + + // Store the callback to prevent it from being dropped + let callback_box = Box::new(callback); + let callback_ptr = Box::into_raw(callback_box); + + let success = unsafe { + sys::ccap_provider_set_new_frame_callback( + self.handle, + Some(new_frame_callback_wrapper), + callback_ptr as *mut c_void, + ) + }; + + if success { + self.callback_ptr = Some(callback_ptr as *mut c_void); + Ok(()) + } else { + // Clean up on failure + unsafe { + let _ = Box::from_raw(callback_ptr); + } + Err(CcapError::InvalidParameter( + "Failed to set frame callback".to_string(), + )) + } + } + + /// Remove frame callback + pub fn remove_new_frame_callback(&mut self) -> Result<()> { + let success = unsafe { + sys::ccap_provider_set_new_frame_callback(self.handle, None, ptr::null_mut()) + }; + + if success { + self.cleanup_callback(); + Ok(()) + } else { + Err(CcapError::CaptureStopFailed) + } + } + + /// Clean up callback pointer + fn cleanup_callback(&mut self) { + if let Some(callback_ptr) = self.callback_ptr.take() { + unsafe { + let _ = Box::from_raw( + callback_ptr as *mut Box bool + Send + Sync>, + ); + } + } + } +} + +impl Drop for Provider { + fn drop(&mut self) { + // Clean up callback first + self.cleanup_callback(); + + if !self.handle.is_null() { + unsafe { + sys::ccap_provider_destroy(self.handle); + } + self.handle = ptr::null_mut(); + } + } +} diff --git a/bindings/rust/src/types.rs b/bindings/rust/src/types.rs new file mode 100644 index 00000000..1160c3ec --- /dev/null +++ b/bindings/rust/src/types.rs @@ -0,0 +1,226 @@ +use crate::sys; + +/// Pixel format enumeration +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PixelFormat { + /// Unknown pixel format + Unknown, + /// NV12 pixel format + Nv12, + /// NV12F pixel format + Nv12F, + /// I420 pixel format + I420, + /// I420F pixel format + I420F, + /// YUYV pixel format + Yuyv, + /// YUYV flipped pixel format + YuyvF, + /// UYVY pixel format + Uyvy, + /// UYVY flipped pixel format + UyvyF, + /// RGB24 pixel format + Rgb24, + /// BGR24 pixel format + Bgr24, + /// RGBA32 pixel format + Rgba32, + /// BGRA32 pixel format + Bgra32, +} + +impl From for PixelFormat { + fn from(format: sys::CcapPixelFormat) -> Self { + match format { + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_UNKNOWN => PixelFormat::Unknown, + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_NV12 => PixelFormat::Nv12, + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_NV12F => PixelFormat::Nv12F, + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_I420 => PixelFormat::I420, + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_I420F => PixelFormat::I420F, + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_YUYV => PixelFormat::Yuyv, + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_YUYV_F => PixelFormat::YuyvF, + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_UYVY => PixelFormat::Uyvy, + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_UYVY_F => PixelFormat::UyvyF, + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_RGB24 => PixelFormat::Rgb24, + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_BGR24 => PixelFormat::Bgr24, + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_RGBA32 => PixelFormat::Rgba32, + sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_BGRA32 => PixelFormat::Bgra32, + _ => PixelFormat::Unknown, + } + } +} + +impl PixelFormat { + /// Convert pixel format to C enum + pub fn to_c_enum(self) -> sys::CcapPixelFormat { + self.into() + } + + /// Create pixel format from C enum + pub fn from_c_enum(format: sys::CcapPixelFormat) -> Self { + format.into() + } + + /// Get string representation of pixel format + pub fn as_str(self) -> &'static str { + match self { + PixelFormat::Unknown => "Unknown", + PixelFormat::Nv12 => "NV12", + PixelFormat::Nv12F => "NV12F", + PixelFormat::I420 => "I420", + PixelFormat::I420F => "I420F", + PixelFormat::Yuyv => "YUYV", + PixelFormat::YuyvF => "YUYV_F", + PixelFormat::Uyvy => "UYVY", + PixelFormat::UyvyF => "UYVY_F", + PixelFormat::Rgb24 => "RGB24", + PixelFormat::Bgr24 => "BGR24", + PixelFormat::Rgba32 => "RGBA32", + PixelFormat::Bgra32 => "BGRA32", + } + } +} + +impl From for sys::CcapPixelFormat { + fn from(val: PixelFormat) -> Self { + match val { + PixelFormat::Unknown => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_UNKNOWN, + PixelFormat::Nv12 => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_NV12, + PixelFormat::Nv12F => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_NV12F, + PixelFormat::I420 => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_I420, + PixelFormat::I420F => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_I420F, + PixelFormat::Yuyv => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_YUYV, + PixelFormat::YuyvF => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_YUYV_F, + PixelFormat::Uyvy => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_UYVY, + PixelFormat::UyvyF => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_UYVY_F, + PixelFormat::Rgb24 => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_RGB24, + PixelFormat::Bgr24 => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_BGR24, + PixelFormat::Rgba32 => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_RGBA32, + PixelFormat::Bgra32 => sys::CcapPixelFormat_CCAP_PIXEL_FORMAT_BGRA32, + } + } +} + +/// Frame orientation enumeration +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameOrientation { + /// Top to bottom orientation + TopToBottom, + /// Bottom to top orientation + BottomToTop, +} + +impl From for FrameOrientation { + fn from(orientation: sys::CcapFrameOrientation) -> Self { + match orientation { + sys::CcapFrameOrientation_CCAP_FRAME_ORIENTATION_TOP_TO_BOTTOM => { + FrameOrientation::TopToBottom + } + sys::CcapFrameOrientation_CCAP_FRAME_ORIENTATION_BOTTOM_TO_TOP => { + FrameOrientation::BottomToTop + } + _ => FrameOrientation::TopToBottom, + } + } +} + +/// Camera property enumeration +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PropertyName { + /// Width property + Width, + /// Height property + Height, + /// Frame rate property + FrameRate, + /// Internal pixel format property + PixelFormatInternal, + /// Output pixel format property + PixelFormatOutput, + /// Frame orientation property + FrameOrientation, +} + +impl PropertyName { + /// Convert property name to C enum + pub fn to_c_enum(self) -> sys::CcapPropertyName { + self.into() + } +} + +impl From for sys::CcapPropertyName { + fn from(prop: PropertyName) -> Self { + match prop { + PropertyName::Width => sys::CcapPropertyName_CCAP_PROPERTY_WIDTH, + PropertyName::Height => sys::CcapPropertyName_CCAP_PROPERTY_HEIGHT, + PropertyName::FrameRate => sys::CcapPropertyName_CCAP_PROPERTY_FRAME_RATE, + PropertyName::PixelFormatInternal => { + sys::CcapPropertyName_CCAP_PROPERTY_PIXEL_FORMAT_INTERNAL + } + PropertyName::PixelFormatOutput => { + sys::CcapPropertyName_CCAP_PROPERTY_PIXEL_FORMAT_OUTPUT + } + PropertyName::FrameOrientation => sys::CcapPropertyName_CCAP_PROPERTY_FRAME_ORIENTATION, + } + } +} + +/// Color conversion backend enumeration +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColorConversionBackend { + /// CPU backend + Cpu, + /// AVX2 backend + Avx2, + /// NEON backend + Neon, + /// Apple Accelerate backend + Accelerate, +} + +impl ColorConversionBackend { + /// Convert backend to C enum + pub fn to_c_enum(self) -> sys::CcapConvertBackend { + match self { + ColorConversionBackend::Cpu => sys::CcapConvertBackend_CCAP_CONVERT_BACKEND_CPU, + ColorConversionBackend::Avx2 => sys::CcapConvertBackend_CCAP_CONVERT_BACKEND_AVX2, + ColorConversionBackend::Neon => sys::CcapConvertBackend_CCAP_CONVERT_BACKEND_NEON, + ColorConversionBackend::Accelerate => { + sys::CcapConvertBackend_CCAP_CONVERT_BACKEND_APPLE_ACCELERATE + } + } + } + + /// Create backend from C enum + pub fn from_c_enum(backend: sys::CcapConvertBackend) -> Self { + match backend { + sys::CcapConvertBackend_CCAP_CONVERT_BACKEND_CPU => ColorConversionBackend::Cpu, + sys::CcapConvertBackend_CCAP_CONVERT_BACKEND_AVX2 => ColorConversionBackend::Avx2, + sys::CcapConvertBackend_CCAP_CONVERT_BACKEND_NEON => ColorConversionBackend::Neon, + sys::CcapConvertBackend_CCAP_CONVERT_BACKEND_APPLE_ACCELERATE => { + ColorConversionBackend::Accelerate + } + _ => ColorConversionBackend::Cpu, + } + } +} + +/// Resolution structure +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Resolution { + /// Width in pixels + pub width: u32, + /// Height in pixels + pub height: u32, +} + +impl From for Resolution { + fn from(res: sys::CcapResolution) -> Self { + Resolution { + width: res.width, + height: res.height, + } + } +} diff --git a/bindings/rust/src/utils.rs b/bindings/rust/src/utils.rs new file mode 100644 index 00000000..810b693a --- /dev/null +++ b/bindings/rust/src/utils.rs @@ -0,0 +1,276 @@ +use crate::error::{CcapError, Result}; +use crate::frame::VideoFrame; +use crate::sys; +use crate::types::PixelFormat; +use std::ffi::CString; +use std::path::Path; + +/// Utility functions +pub struct Utils; + +impl Utils { + /// Convert pixel format enum to string + pub fn pixel_format_to_string(format: PixelFormat) -> Result { + let mut buffer = [0i8; 64]; + let result = unsafe { + sys::ccap_pixel_format_to_string(format.to_c_enum(), buffer.as_mut_ptr(), buffer.len()) + }; + + if result < 0 { + return Err(CcapError::StringConversionError( + "Unknown pixel format".to_string(), + )); + } + + let c_str = unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) }; + c_str.to_str().map(|s| s.to_string()).map_err(|_| { + CcapError::StringConversionError("Invalid pixel format string".to_string()) + }) + } + + /// Convert string to pixel format enum + pub fn string_to_pixel_format(format_str: &str) -> Result { + // This function doesn't exist in C API, we'll implement a simple mapping + match format_str.to_lowercase().as_str() { + "unknown" => Ok(PixelFormat::Unknown), + "nv12" => Ok(PixelFormat::Nv12), + "nv12f" => Ok(PixelFormat::Nv12F), + "i420" => Ok(PixelFormat::I420), + "i420f" => Ok(PixelFormat::I420F), + "yuyv" => Ok(PixelFormat::Yuyv), + "yuyvf" => Ok(PixelFormat::YuyvF), + "uyvy" => Ok(PixelFormat::Uyvy), + "uyvyf" => Ok(PixelFormat::UyvyF), + "rgb24" => Ok(PixelFormat::Rgb24), + "bgr24" => Ok(PixelFormat::Bgr24), + "rgba32" => Ok(PixelFormat::Rgba32), + "bgra32" => Ok(PixelFormat::Bgra32), + _ => Err(CcapError::StringConversionError( + "Unknown pixel format string".to_string(), + )), + } + } + + /// Save frame as BMP file + pub fn save_frame_as_bmp>(frame: &VideoFrame, file_path: P) -> Result<()> { + // This function doesn't exist in C API, we'll use the dump_frame_to_file instead + Self::dump_frame_to_file(frame, file_path)?; + Ok(()) + } + + /// Convert path to C string safely, handling Windows-specific path issues + fn path_to_cstring>(path: P) -> Result { + #[cfg(windows)] + { + // On Windows, handle potential UTF-16 to UTF-8 conversion issues + let path_str = path.as_ref().to_string_lossy(); + CString::new(path_str.as_bytes()) + .map_err(|_| CcapError::StringConversionError("Invalid file path".to_string())) + } + + #[cfg(not(windows))] + { + // On Unix-like systems, standard conversion should work + let path_str = path + .as_ref() + .to_str() + .ok_or_else(|| CcapError::StringConversionError("Invalid file path".to_string()))?; + CString::new(path_str) + .map_err(|_| CcapError::StringConversionError("Invalid file path".to_string())) + } + } + + /// Save a video frame to a file with automatic format detection + pub fn dump_frame_to_file>( + frame: &VideoFrame, + filename_no_suffix: P, + ) -> Result { + let c_path = Self::path_to_cstring(filename_no_suffix)?; + + // First call to get required buffer size + let buffer_size = unsafe { + sys::ccap_dump_frame_to_file(frame.as_c_ptr(), c_path.as_ptr(), std::ptr::null_mut(), 0) + }; + + if buffer_size <= 0 { + return Err(CcapError::FileOperationFailed( + "Failed to dump frame to file".to_string(), + )); + } + + // Second call to get actual result + let mut buffer = vec![0u8; buffer_size as usize]; + let result_len = unsafe { + sys::ccap_dump_frame_to_file( + frame.as_c_ptr(), + c_path.as_ptr(), + buffer.as_mut_ptr() as *mut i8, + buffer.len(), + ) + }; + + if result_len <= 0 { + return Err(CcapError::FileOperationFailed( + "Failed to dump frame to file".to_string(), + )); + } + + // Convert to string + buffer.truncate(result_len as usize); + String::from_utf8(buffer) + .map_err(|_| CcapError::StringConversionError("Invalid output path string".to_string())) + } + + /// Save a video frame to directory with auto-generated filename + pub fn dump_frame_to_directory>( + frame: &VideoFrame, + directory: P, + ) -> Result { + let c_dir = Self::path_to_cstring(directory)?; + + // First call to get required buffer size + let buffer_size = unsafe { + sys::ccap_dump_frame_to_directory( + frame.as_c_ptr(), + c_dir.as_ptr(), + std::ptr::null_mut(), + 0, + ) + }; + + if buffer_size <= 0 { + return Err(CcapError::FileOperationFailed( + "Failed to dump frame to directory".to_string(), + )); + } + + // Second call to get actual result + let mut buffer = vec![0u8; buffer_size as usize]; + let result_len = unsafe { + sys::ccap_dump_frame_to_directory( + frame.as_c_ptr(), + c_dir.as_ptr(), + buffer.as_mut_ptr() as *mut i8, + buffer.len(), + ) + }; + + if result_len <= 0 { + return Err(CcapError::FileOperationFailed( + "Failed to dump frame to directory".to_string(), + )); + } + + // Convert to string + buffer.truncate(result_len as usize); + String::from_utf8(buffer) + .map_err(|_| CcapError::StringConversionError("Invalid output path string".to_string())) + } + + /// Save RGB data as BMP file (generic version) + #[allow(clippy::too_many_arguments)] + pub fn save_rgb_data_as_bmp>( + filename: P, + data: &[u8], + width: u32, + stride: u32, + height: u32, + is_bgr: bool, + has_alpha: bool, + is_top_to_bottom: bool, + ) -> Result<()> { + let c_path = Self::path_to_cstring(filename)?; + + let success = unsafe { + sys::ccap_save_rgb_data_as_bmp( + c_path.as_ptr(), + data.as_ptr(), + width, + stride, + height, + is_bgr, + has_alpha, + is_top_to_bottom, + ) + }; + + if success { + Ok(()) + } else { + Err(CcapError::FileOperationFailed( + "Failed to save RGB data as BMP".to_string(), + )) + } + } + + /// Interactive camera selection helper + pub fn select_camera(devices: &[String]) -> Result { + if devices.is_empty() { + return Err(CcapError::DeviceNotFound); + } + + if devices.len() == 1 { + println!("Using the only available device: {}", devices[0]); + return Ok(0); + } + + println!("Multiple devices found, please select one:"); + for (i, device) in devices.iter().enumerate() { + println!(" {}: {}", i, device); + } + + print!("Enter the index of the device you want to use: "); + use std::io::{self, Write}; + io::stdout().flush().unwrap(); + + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .map_err(|e| CcapError::InvalidParameter(format!("Failed to read input: {}", e)))?; + + let selected_index = input.trim().parse::().unwrap_or(0); + + if selected_index >= devices.len() { + println!("Invalid index, using the first device: {}", devices[0]); + Ok(0) + } else { + println!("Using device: {}", devices[selected_index]); + Ok(selected_index) + } + } + + /// Set log level + pub fn set_log_level(level: LogLevel) { + unsafe { + sys::ccap_set_log_level(level.to_c_enum()); + } + } +} + +/// Log level enumeration +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LogLevel { + /// No log output + None, + /// Error log level + Error, + /// Warning log level + Warning, + /// Info log level + Info, + /// Verbose log level + Verbose, +} + +impl LogLevel { + /// Convert log level to C enum + pub fn to_c_enum(self) -> sys::CcapLogLevel { + match self { + LogLevel::None => sys::CcapLogLevel_CCAP_LOG_LEVEL_NONE, + LogLevel::Error => sys::CcapLogLevel_CCAP_LOG_LEVEL_ERROR, + LogLevel::Warning => sys::CcapLogLevel_CCAP_LOG_LEVEL_WARNING, + LogLevel::Info => sys::CcapLogLevel_CCAP_LOG_LEVEL_INFO, + LogLevel::Verbose => sys::CcapLogLevel_CCAP_LOG_LEVEL_VERBOSE, + } + } +} diff --git a/bindings/rust/tests/integration_tests.rs b/bindings/rust/tests/integration_tests.rs new file mode 100644 index 00000000..4336575b --- /dev/null +++ b/bindings/rust/tests/integration_tests.rs @@ -0,0 +1,90 @@ +//! Integration tests for ccap rust bindings +//! +//! Tests the main API functionality + +use ccap::{CcapError, PixelFormat, Provider, Result}; + +fn skip_camera_tests() -> bool { + std::env::var("CCAP_SKIP_CAMERA_TESTS").is_ok() +} + +#[test] +fn test_provider_creation() -> Result<()> { + let provider = Provider::new()?; + assert!(!provider.is_opened()); + Ok(()) +} + +#[test] +fn test_library_version() -> Result<()> { + let version = ccap::version()?; + assert!(!version.is_empty()); + assert!(version.contains('.')); + println!("ccap version: {}", version); + Ok(()) +} + +#[test] +fn test_device_listing() -> Result<()> { + if skip_camera_tests() { + eprintln!("Skipping device_listing due to CCAP_SKIP_CAMERA_TESTS"); + return Ok(()); + } + let provider = Provider::new()?; + let devices = provider.list_devices()?; + // In test environment we might not have cameras, so just check it doesn't crash + println!("Found {} devices", devices.len()); + for (i, device) in devices.iter().enumerate() { + println!("Device {}: {}", i, device); + } + Ok(()) +} + +#[test] +fn test_pixel_format_conversion() { + let format = PixelFormat::Rgb24; + let c_format = format.to_c_enum(); + let format_back = PixelFormat::from_c_enum(c_format); + assert_eq!(format, format_back); +} + +#[test] +fn test_error_types() { + let error = CcapError::NoDeviceFound; + let error_str = format!("{}", error); + assert!(error_str.contains("No camera device found")); +} + +#[test] +fn test_provider_with_index() { + if skip_camera_tests() { + eprintln!("Skipping provider_with_index due to CCAP_SKIP_CAMERA_TESTS"); + return; + } + // This might fail if no device at index 0, but should not crash + match Provider::with_device(0) { + Ok(_provider) => { + println!("Successfully created provider with device 0"); + } + Err(e) => { + println!("Expected error for device 0: {}", e); + } + } +} + +#[test] +fn test_device_operations_without_camera() { + if skip_camera_tests() { + eprintln!("Skipping device_operations_without_camera due to CCAP_SKIP_CAMERA_TESTS"); + return; + } + // Test that operations work regardless of camera presence + let provider = Provider::new().expect("Failed to create provider"); + + // These should work with or without cameras + let devices = provider.list_devices().expect("Failed to list devices"); + println!("Found {} device(s)", devices.len()); + + let version = Provider::version().expect("Failed to get version"); + assert!(!version.is_empty()); +} diff --git a/bindings/rust/wrapper.h b/bindings/rust/wrapper.h new file mode 100644 index 00000000..835170c4 --- /dev/null +++ b/bindings/rust/wrapper.h @@ -0,0 +1,3 @@ +#include "ccap_c.h" +#include "ccap_utils_c.h" +#include "ccap_convert_c.h" diff --git a/scripts/update_version.sh b/scripts/update_version.sh index 39c03a5c..b800896e 100755 --- a/scripts/update_version.sh +++ b/scripts/update_version.sh @@ -91,4 +91,28 @@ update_file "s/version = \".*\"/version = \"$NEW_VERSION\"/" "$PROJECT_ROOT/cona # 4. Update BUILD_AND_INSTALL.md (documentation) update_file "s/Current version: .*/Current version: $NEW_VERSION/" "$PROJECT_ROOT/BUILD_AND_INSTALL.md" "BUILD_AND_INSTALL.md" +# 5. Update Rust crate version (Cargo.toml) +update_file "s/^version = \".*\"$/version = \"$NEW_VERSION\"/" "$PROJECT_ROOT/bindings/rust/Cargo.toml" "Rust crate version (Cargo.toml)" + +# 6. Update Rust lockfile entry for ccap (Cargo.lock) +RUST_LOCKFILE="$PROJECT_ROOT/bindings/rust/Cargo.lock" +if [ -f "$RUST_LOCKFILE" ]; then + python3 - "$RUST_LOCKFILE" "$NEW_VERSION" <<'PY' +import pathlib, re, sys + +path = pathlib.Path(sys.argv[1]) +new_ver = sys.argv[2] +text = path.read_text() +pattern = r'(?m)(^name = "ccap"\nversion = ")[^"]+("\n)' +new_text, count = re.subn(pattern, rf"\1{new_ver}\2", text, count=1) +if count: + path.write_text(new_text) + print(f"✅ Updated Rust lockfile ccap version to {new_ver}") +else: + print(f"⚠️ ccap entry not found in {path}, lockfile not modified") +PY +else + echo "⚠️ $RUST_LOCKFILE not found, skipping Rust lockfile update" +fi + echo "Version update complete!" diff --git a/src/ccap_convert_frame.cpp b/src/ccap_convert_frame.cpp index 42c6f36c..c9d9f789 100644 --- a/src/ccap_convert_frame.cpp +++ b/src/ccap_convert_frame.cpp @@ -242,7 +242,7 @@ bool inplaceConvertFrame(VideoFrame* frame, PixelFormat toFormat, bool verticalF if (ret) { assert(frame->pixelFormat == toFormat); assert(frame->allocator != nullptr && frame->data[0] == frame->allocator->data()); - frame->sizeInBytes = frame->allocator->size(); + frame->sizeInBytes = static_cast(frame->allocator->size()); } return ret; }