Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Camera binding #2150

Merged
merged 38 commits into from
Nov 9, 2023
Merged

Camera binding #2150

merged 38 commits into from
Nov 9, 2023

Conversation

raffaeler
Copy link
Contributor

@raffaeler raffaeler commented Oct 17, 2023

This PR adds a new binding to transparently capture still pictures or video streams using the apps installed by default in the OS.
One of the setting factories specifically targets the Raspbian OS. This allows to transparently use the legacy raspicam stack or the newest libcamera stack using the apps provided by the OS and capturing the stdio output containing the binary data.

The Camera.Samples project shows how to:

  • list the installed cameras (only on the libcamera stack)
  • capture a still using raspistill
  • capture a video using raspivid
  • capture a sequence of pictures at a regular interval (time-lapse) using raspistill
  • capture a still using libcamera-still
  • capture a video using libcamera-vid
  • capture a sequence of pictures at a regular interval (time-lapse) using libcamera-still

The unit tests use a console application to test the communication channel via stdout/stdin.
The sample application has been tested on both Buster (raspicam) and Bullseye (libcamera).

Reference issues:

Microsoft Reviewers: Open in CodeFlow

@ghost ghost added the area-device-bindings Device Bindings for audio, sensor, motor, and display hardware that can used with System.Device.Gpio label Oct 17, 2023
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DefaultSampleTfms)</TargetFramework>
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be $(DefaultBindingTfms)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for your help @krwq. Now everything is ok.

Copy link
Member

Choose a reason for hiding this comment

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

not sure why you have the button part of this PR :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No idea, it was on a different branch :-(

public class ProcessRunnerTests
{
private const string FakeVideocapture =
@"..\..\..\..\FakeVideoCapture\bin\Debug\net6.0\FakeVideoCapture.exe";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not sure this is the best way in our repo to reference the external process as this string contains the net6.0 tfm.

An alternative would be to reference the project from the unit tests so that the exe is copied in its binary folder.

Other ideas?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, you can for example do the following:

  • search for this file in a recursive way up to the moment you'll find one.
  • Or, you can as well copy it into your test project. You can then set it to copy in the artifact. You just need to build the FakeVideoCapture project first.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you @Ellerbach

  • I was going with the option 1 but there no Initialization phase in xunit
  • Not sure how to do the option 2 (both phases)

But I tried to simply add a reference to the exe and it gets copied to the unit test project binary folder.
Do you see problems with this solution?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

BTW I just committed this solution as it looks good.

@krwq
Copy link
Member

krwq commented Oct 19, 2023

[Triage] See if we can reuse process runner

process.Start();

@raffaeler
Copy link
Contributor Author

[Triage] See if we can reuse process runner

process.Start();

Yes, it can replace the code in the RTC as it use the same ProcessStartInfo settings.
The ProcessRunner does more:

  • deals with text or binary data
  • implements the async pattern
  • deals with continuous capturing that can be programmatically stopped
  • Expose an overload that takes a PipeWriter instead of a Stream to simplify consuming complex data

Given we can reuse the ProcessRunner, what's the most appropriate place for it? (Assembly / Namespace). /cc @krwq

@pgrawehr
Copy link
Contributor

Given we can reuse the ProcessRunner, what's the most appropriate place for it? (Assembly / Namespace).

I'd just add it to the Iot.Device.Common namespace( devices\Common project)

Copy link
Member

@Ellerbach Ellerbach left a comment

Choose a reason for hiding this comment

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

Nice binding! Comments mainly regarding intellisense and couple of other quite minor elements

}
}
catch (Exception)
{
Copy link
Member

Choose a reason for hiding this comment

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

So can you then just add a note that there is nothing to do in the catch and it's on purpose for this reason?

/// the Environment.CurrentDirectory.
/// The working directory is important when referring to files from the command line.
/// </summary>
public string GetFullCommandLine(string[] arguments)
Copy link
Member

Choose a reason for hiding this comment

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

you are missing the return type and description in the intellisense comment.
And the parameters as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

I have found that we haven't enabled the "Return value must be documented" stylecop rule. I'm going to enable it, but I need to fix quite a bunch of comments to do so.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I hope I didn't forget any of them, but it was an easy but long work

Copy link
Member

Choose a reason for hiding this comment

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

When using Visual Studio, the params, the return and whatever else including exceptions are added automatically, so it makes life easier! Also, with copilot, most of the comments can be done automatically with looking at the result.
When developing, just disable the rules for comments and activate it back once you're done and create the intellisense comments at once.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please note that stylecop does not warn about the missing comment for parameters in addition to the return type.

When using Visual Studio, the params, the return and whatever else including exceptions are added automatically

I know, but every company has different ruleset and when I write code sometimes I jump from one style to another. Since VS did not complain, I committed without the other set of comments.

/// Execute the process with a number of arguments. The target
/// Stream receives the stdout of the process
/// </summary>
public Task ExecuteAsync(string[] arguments, Stream? target)
Copy link
Member

Choose a reason for hiding this comment

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

you are missing the return type and description in the intellisense comment.
And the parameters as well.

/// If the process is not expected to return any output (for example when
/// the app directly writes one or more files), the stream can be null
/// </summary>
public async Task ExecuteAsync(string argsString, Stream? target)
Copy link
Member

Choose a reason for hiding this comment

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

you are missing the return type and description in the intellisense comment.
And the parameters as well.

/// Execute the process with the given arguments and
/// returns the output as a string, decoded as UTF8
/// </summary>
public async Task<string> ExecuteReadOutputAsStringAsync(string[] arguments)
Copy link
Member

Choose a reason for hiding this comment

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

you are missing the return type and description in the intellisense comment.
And the parameters as well.

[Fact]
public async Task TestBinary2()
{
/* CreateTestFile(Video1); */
Copy link
Member

Choose a reason for hiding this comment

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

why is this part commented?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is the code that I used to create the test.bin file that is added in the project.
With this code the file can be re-created.

Copy link
Member

Choose a reason for hiding this comment

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

Can you then please add a little comment about is in the code?

[Fact]
public async Task TestText1()
{
/* CreateTestFile(Video1); */
Copy link
Member

Choose a reason for hiding this comment

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

Why is this code commented?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as before.

[Fact]
public async Task TestText2()
{
/* CreateTestFile(Video1); */
Copy link
Member

Choose a reason for hiding this comment

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

why is this code commented?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as before.

Copy link
Member

Choose a reason for hiding this comment

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

So please add the same comment :-D

Made the required review changes on the comments.
Fixed the SkiaSharpAdapter in Media.sln
@raffaeler
Copy link
Contributor Author

@Ellerbach apart the comments where I responded differently, I completed the requested changes.
As for the triage, I also moved the ProcessRunner and ProcessRunnerSettings into the Common assembly.

src/devices/Camera/Camera/CameraInfo.cs Show resolved Hide resolved
src/devices/Camera/Camera/CameraInfo.cs Show resolved Hide resolved
public class LibcameraAppsSettings
{
/// <summary>
/// The default options for the Libcamera-apps
Copy link
Member

Choose a reason for hiding this comment

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

Very minimal: in theory, properties comments should start with "Gets or sets" ("Gets" only in this case")

@@ -0,0 +1,293 @@
# The Camera binding
Copy link
Member

Choose a reason for hiding this comment

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

You may have skip this feedback but I would definitely do 2 different pages. 1 simple README for the usage and a more detailed one for the technicalities of the choices. So for a user it stays simple and straight forward.

Copy link
Member

@Ellerbach Ellerbach left a comment

Choose a reason for hiding this comment

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

Very little things and then good to go! Approving already :-)

[Fact]
public async Task TestBinary2()
{
/* CreateTestFile(Video1); */
Copy link
Member

Choose a reason for hiding this comment

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

Can you then please add a little comment about is in the code?

[Fact]
public async Task TestText2()
{
/* CreateTestFile(Video1); */
Copy link
Member

Choose a reason for hiding this comment

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

So please add the same comment :-D

@raffaeler
Copy link
Contributor Author

@Ellerbach I splitted the README.md in two documents. The other one is called CameraInsights.md

@Ellerbach
Copy link
Member

Ellerbach commented Nov 3, 2023

@Ellerbach I splitted the README.md in two documents. The other one is called CameraInsights.md

All good for me! It's much better from a user point of view. All good for the rest and good to go for me (once the build will be fixed)!

@raffaeler
Copy link
Contributor Author

@Ellerbach I splitted the README.md in two documents. The other one is called CameraInsights.md

All good for me! It's much better from a user point of view. All good for the rest and good to go for me (once the build will be fixed)!

@Ellerbach now it's ok. The markdown linter complained a microsoft url not reachable (on another binding) and a couple of line feed at the end of the documents (my visual editor always modifies them).

/// </summary>
/// <param name="ms">The length of the capture operation in milliseconds.</param>
/// <returns>A reference to this instance.</returns>
public CommandOptionsBuilder WithContinuousStreaming(int ms = 0)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: It would be really nice to convert these args to TimeSpan where possible but feel free to do it separately or ignore this since it's minor feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you don't mind I would discuss this in a later PR because I could not find a way to specify the default values as const
(TimeSpan.Zero is not const and 1 ms as well)

Copy link
Contributor

Choose a reason for hiding this comment

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

The following should work as default arguments: new TimeSpan(0) or new TimeSpan(valueAs100nsTicks). As an alternative, you could use overloading instead of default arguments.

But I agree that's something that could be done separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, let's discuss it for later then :)

/// </summary>
/// <param name="quality">The quality of each MJPEG picture.</param>
/// <returns>A reference to this instance.</returns>
public CommandOptionsBuilder WithMJPEGVideoOptions(int quality)
Copy link
Member

Choose a reason for hiding this comment

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

nit: we don't do fluent APIs here elsewhere (I think), feel free to ignore this comment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure, I did it just for uniformity ... also it felt easier when using it in the sample/test code

/// </summary>
/// <returns>The test task operation.</returns>
[Fact]
public async Task TestCameraList()
Copy link
Member

Choose a reason for hiding this comment

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

this test seems camera specific, will this work with any camera?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test is meant to validate the parser using the text.txt content grabbed from my RPi.
The output you obtain with --list-cameras is always formatted using the same pattern (as far as I could see).

@krwq krwq merged commit a5181f5 into dotnet:main Nov 9, 2023
9 checks passed
@pgrawehr pgrawehr added this to the v3.1.0 milestone Nov 9, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Dec 10, 2023
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-device-bindings Device Bindings for audio, sensor, motor, and display hardware that can used with System.Device.Gpio
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants