Skip to content

feat(vdo): Add safe Rust bindings for VDO API#223

Open
vsem-azamat wants to merge 3 commits intoAxisCommunications:mainfrom
vsem-azamat:feature/vdo-completion
Open

feat(vdo): Add safe Rust bindings for VDO API#223
vsem-azamat wants to merge 3 commits intoAxisCommunications:mainfrom
vsem-azamat:feature/vdo-completion

Conversation

@vsem-azamat
Copy link

Safe Rust bindings for VDO (Video Capture) API

Safe wrappers over vdo-sys with builder pattern for stream creation, iterator-based frame capture, and automatic resource cleanup.

Tested on device: YUV, JPEG, H.264, H.265 formats work correctly.

Describe your changes

This PR adds a new vdo crate providing safe Rust bindings for the VDO API, enabling video capture from Axis cameras.

Based on the work from PR #153 by @JsGjKJzi — completed the implementation, added comprehensive tests, and updated the example application.

Note: @JsGjKJzi hasn't responded since January 2024.

Key features:

  • StreamBuilder with builder pattern for configuring video streams
  • Stream, RunningStream, StreamIterator for lifecycle management
  • StreamBuffer and Frame for zero-copy frame access
  • Automatic resource cleanup via Drop implementations
  • Comprehensive error handling with VdoError including error code names
  • Platform compatibility documentation (Artpec-6/7/8/9, Ambarella CV)

Changes:

  • New crates/vdo/ crate with safe bindings
  • Updated apps/vdo_encode_client/ to use the new vdo crate
  • Unit tests (no device required) + device tests (gated by device-tests feature)

Issue ticket number and link

Checklist before requesting a review

  • I have performed a self-review of my own code
  • I have verified that the code builds perfectly fine on my local system
  • I have added tests that prove my fix is effective or that my feature works
  • I have commented my code, particularly in hard-to-understand areas
  • I have verified that my code follows the style already available in the repository
  • I have made corresponding changes to the documentation

Safe wrappers over vdo-sys with builder pattern for stream creation,
iterator-based frame capture, and automatic resource cleanup.

Tested on device: YUV, JPEG, H.264, H.265 formats work correctly.
Copilot AI review requested due to automatic review settings December 28, 2025 15:09
@vsem-azamat vsem-azamat requested a review from a team as a code owner December 28, 2025 15:09
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds safe Rust bindings for the VDO (Video Capture) API, enabling video capture from Axis cameras. The implementation provides a builder pattern for stream configuration, iterator-based frame capture, and automatic resource cleanup through RAII principles.

Key changes:

  • New vdo crate with safe abstractions over vdo-sys FFI bindings
  • Comprehensive error handling with detailed error code mapping
  • Builder pattern for ergonomic stream configuration
  • Updated vdo_encode_client application demonstrating multiple video formats

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
crates/vdo/src/lib.rs Core library implementation with safe wrappers for VDO API, including Stream, StreamBuffer, Frame types, and comprehensive tests
crates/vdo/examples/basic.rs Simple example demonstrating video stream capture with YUV format
crates/vdo/Cargo.toml Package configuration for the new vdo crate with device-tests feature flag
apps/vdo_encode_client/src/main.rs Updated example application testing multiple video formats (YUV, JPEG, H.264, H.265)
apps/vdo_encode_client/Cargo.toml Updated dependency from vdo-sys to the new vdo crate
Cargo.toml Added vdo workspace member
Cargo.lock Updated lock file with vdo crate dependencies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vsem-azamat
Copy link
Author

I'll wait for feedback from the maintainers before addressing these suggestions, as some may be more relevant than others for this codebase.

@apljungquist
Copy link
Collaborator

I won't have time to look at the code before new year, but I would like to say already now that I'm happy you are interested in carrying on the work that Jonathan started 🙂

Copy link
Collaborator

@apljungquist apljungquist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I skimmed the PR and I think it looks pretty good. I'll try to do a more thorough read in the before next weekend focusing on safety and I'll try to respond to comments within 24h.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this example program equivalent to it's namesake from acap-native-sdk-examples?

It's OK if it is not, but then we should rename the example since the current name reflects the intention that it is a port of the C example (#183), like many other examples are ports of their namesake C example.

use log::{error, info};
use vdo::{Error, Stream, VdoFormat};

fn test_format(name: &str, format: VdoFormat, num_frames: usize) -> Result<(), Error> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That this is called test_* makes me wonder if perhaps there is a way to express it as a unit test. That way we can ensure that it doesn't break semi-automatically (I say semi because one still has to set up a compatible device etc). Is this something that you have explored?

Copy link
Collaborator

@apljungquist apljungquist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some API design related questions.

API design is difficult and typically subjective. I don't want it to stall this PR, so consider this inspiration. (that's not to say that the previous review was not subjective or disputable, only that this is even more so)

pub struct StreamBuilder {
format: VdoFormat,
buffer_count: u32,
buffer_strategy: VdoBufferStrategy,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a use case for the explicit buffer strategy? In the comments in the base PR, #153 (comment) using VDO_BUFFER_STRATEGY_INFINITE is recommended so I think we can omit EXPLICIT entirely which would simplify the API.

/// Default: `VdoBufferStrategy::VDO_BUFFER_STRATEGY_INFINITE`
///
/// - `VDO_BUFFER_STRATEGY_INFINITE`: VDO manages buffers internally (works for all formats)
/// - `VDO_BUFFER_STRATEGY_EXPLICIT`: Application manages buffers (only for YUV/RGB)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If EXPLICIT strategy is requested the documented invariant should be checked in the build method so we can return an appropriate error variant.

.to_string()
};

// Free the GError
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should be re-formulated as a safety comment as suggested by @apljungquist above. In it's current form it is not very useful since it just re-states what the code does.


impl Map {
/// Creates a new empty map.
pub fn new() -> Result<Self> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We typically prefix fallible constructors with try, see below.

Suggested change
pub fn new() -> Result<Self> {
pub fn try_new() -> Result<Self> {

}

/// Returns the frame data as a mutable byte slice.
pub fn as_mut_slice(&mut self) -> Result<&mut [u8]> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When should the as_mut_slice method be used?

I am uncertain if writing to this memory from the application side is sound and since the buffers are a limited resource. I think a safer API would be to provide a clone implementation that copies the data which the application can then write to if needed.

/// This can be used for zero-copy operations with other APIs.
pub fn file_descriptor(&self) -> std::os::fd::BorrowedFd<'_> {
unsafe {
let fd = vdo_frame_get_fd(self.raw);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// Metadata for a video frame.
///
/// Contains information about frame timing, size, and type.
pub struct Frame<'a> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider merging the Frame and StreamBuffer structs since their lifetimes are very much linked.
As stated in https://developer.axis.com/acap/api/src/api/vdostream/html/vdo-buffer_8h.html#a5367ff8edb99976f0b22a2809d44c097, the frame object is only valid while the buffer is so I'm not convinced it needs a separate type when we could just add relevant methods to the StreamBuffer type.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not saying we should depend on this, but no shit they are linked: typedef VdoFrame VdoBuffer; 😅


/// Returns the file descriptor for the frame data.
///
/// This can be used for zero-copy operations with other APIs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think using the FD is related to zero-copy; the C api documentation makes no mention of it: https://developer.axis.com/acap/api/src/api/vdostream/html/vdo-frame_8h.html#ab4bb3b7b0775ce6caee5172af24a16fd

Do you have an application in mind where you intend to use the file descriptor?

pub struct StreamBuffer<'a> {
raw: *mut VdoBuffer,
stream: &'a Stream,
_phantom: PhantomData<&'a ()>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need the phantomdata here since you already have a borrowed reference to the stream which should prevent the buffer from outliving the stream.

@apljungquist
Copy link
Collaborator

I created this to help with my understanding of VDO, posting it here in case you find it usefule: https://github.com/apljungquist/acap-rs/blob/f778576222874210e51f0c46ab4d6ffbdd877572/crates/vdo-sys/README.md

Copy link
Collaborator

@apljungquist apljungquist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have focused this review on the Map type, which looks good. Once we have decided if/how to change it I will continue reviewing other parts.

I do this to not overwhelm you with comments and to not overwhelm myself with different discussions to keep track of and large diffs that need to be reviewed with every now commit.

vsem-azamat added a commit to vsem-azamat/acap-rs that referenced this pull request Feb 10, 2026
Address all review comments. Fix GObject leak in Stream::drop.

- Consume-by-value ownership: start(self), stop(self), unref(self)
- Replace Iterator with next_buffer() -> Result
- Merge Frame into StreamBuffer (VdoBuffer = VdoFrame)
- Extract Map to separate module, keys as &CStr
- Add Resolution enum, remove buffer_strategy, rename to custom_timestamp_us
- Replace as_mut_slice with data_copy(), file_descriptor() returns Result

Tested: cargo clippy clean, 33/33 tests pass on Artpec-9 device.
@vsem-azamat vsem-azamat force-pushed the feature/vdo-completion branch from 2326701 to 70e2962 Compare February 10, 2026 20:47
@vsem-azamat
Copy link
Author

@apljungquist @guoxe
I've tried to fix all things. Pls, review

@apljungquist
Copy link
Collaborator

Will try to get around to this tomorrow

apljungquist and others added 2 commits February 12, 2026 23:21
Address all review comments. Fix GObject leak in Stream::drop.

- Consume-by-value ownership: start(self), stop(self), unref(self)
- Replace Iterator with next_buffer() -> Result
- Merge Frame into StreamBuffer (VdoBuffer = VdoFrame)
- Extract Map to separate module, keys as &CStr
- Add Resolution enum, remove buffer_strategy, rename to custom_timestamp_us
- Replace as_mut_slice with data_copy(), file_descriptor() returns Result

Tested: cargo clippy clean, 33/33 tests pass on Artpec-9 device.
@apljungquist apljungquist force-pushed the feature/vdo-completion branch from 70e2962 to 2326701 Compare February 12, 2026 22:22
@apljungquist
Copy link
Collaborator

apljungquist commented Feb 12, 2026

@vsem-azamat nice progress!

Please refrain from force pushing in the future as it makes it difficult to review the changes and keep track of comments. I took the liberty of cherry-picking your fix onto the branch before it was force pushed and force pushing that, so now the git history looks as it should.

A few of my comments seem to have gone unaddressed. You don't have to always to things the way I think is the best, but I would appreciate if you at least acknowledge on each comment that you have read it and state what, if anything, you will do to address it 🙂

I haven't seen the results of the CI yet, but if we get more revisions in on this I will make sure to start it as soon as I can instead of waiting until I have time to do human review like this time around.

@apljungquist
Copy link
Collaborator

apljungquist commented Feb 13, 2026

It looks like something changed with github actions and it now hangs after starting the dev container: [2026-02-13T18:09:55.811Z] Container started 😮‍💨

I will try to look into this in the next week or so. In the meanwhile I encourage you to run the same check locally so that we have a good PR ready to go when CI gets fixed. The check that compares checksums can be a bit unreliable and is easy to fix, so if that is the only one that is failing we should be all right.

Copy link
Collaborator

@apljungquist apljungquist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to submit this review, sorry. Among the new comments I only feel strongly about the glob import, but as I said, please respond to all new and old comments.

use std::marker::PhantomData;
use std::mem;
use std::ptr;
use vdo_sys::*;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid glob imports as they both make it more difficult to see where a type is coming from and risks importing unexpected traits.

@@ -1315,45 +1245,69 @@ mod device_tests {
fn error_message_is_descriptive() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For tests that verify that an output is subjectively good for a human, I have found that expect-test is a useful tool. You don't have to, but if you feel like looking into it I think it could be useful in this test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments