Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/devices/Camera/Camera/Camera.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,11 @@
<PackageReference Include="System.IO.Pipelines" Version="$(SystemIOPipelinesVersion)" />
</ItemGroup>

<!-- Make the internal members visible to the unit test assembly -->
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>TestCamera</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

</Project>
116 changes: 116 additions & 0 deletions src/devices/Camera/Camera/Settings/ProcessSettingsFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -36,6 +37,29 @@ public static class ProcessSettingsFactory
/// </summary>
public const string LibcameraVid = "libcamera-vid";

/// <summary>
/// The process name of the rpicam application used to capture still pictures on the Raspbian OS.
/// This is the new name for the <see cref="LibcameraStill"/> application introduced with Raspberry Pi OS Bookworm.
/// </summary>
public const string RpicamStill = "rpicam-still";

/// <summary>
/// The process name of the rpicam application used to capture video streams on the Raspbian OS.
/// This is the new name for the <see cref="LibcameraVid"/> application introduced with Raspberry Pi OS Bookworm.
/// </summary>
public const string RpicamVid = "rpicam-vid";

/// <summary>
/// Returns true when the new rpicam-apps (rpicam-still / rpicam-vid) are available on the system path.
/// Starting with Raspberry Pi OS Bookworm the libcamera-* applications have been renamed to rpicam-*.
/// </summary>
/// <returns>True if the rpicam-apps are installed, otherwise false.</returns>
public static bool IsRpicamAppsInstalled()
=> IsRpicamAppsInstalled(IsApplicationOnPath);

internal static bool IsRpicamAppsInstalled(Func<string, bool> applicationExists)
=> applicationExists(RpicamStill) || applicationExists(RpicamVid);

/// <summary>
/// Creates a ProcessSettings instance targeting raspistill.
/// </summary>
Expand Down Expand Up @@ -91,4 +115,96 @@ public static ProcessSettings CreateForLibcameravid()
settings.Filename = LibcameraVid;
return settings;
}

/// <summary>
/// Creates a ProcessSettings instance targeting rpicam-still and capturing stderr.
/// </summary>
/// <returns>An instance of the <see cref="ProcessSettings"/> class</returns>
public static ProcessSettings CreateForRpicamstillAndStderr()
{
var settings = new ProcessSettings();
settings.Filename = RpicamStill;
settings.CaptureStderrInsteadOfStdout = true;
return settings;
}

/// <summary>
/// Creates a ProcessSettings instance targeting rpicam-still.
/// </summary>
/// <returns>An instance of the <see cref="ProcessSettings"/> class</returns>
public static ProcessSettings CreateForRpicamstill()
{
var settings = new ProcessSettings();
settings.Filename = RpicamStill;
return settings;
}

/// <summary>
/// Creates a ProcessSettings instance targeting rpicam-vid.
/// </summary>
/// <returns>An instance of the <see cref="ProcessSettings"/> class</returns>
public static ProcessSettings CreateForRpicamvid()
{
var settings = new ProcessSettings();
settings.Filename = RpicamVid;
return settings;
}

/// <summary>
/// Creates a ProcessSettings instance for capturing still pictures using the libcamera/rpicam stack
/// and capturing stderr. The new rpicam-still application is used when available, otherwise the
/// legacy libcamera-still name is used (which remains available as a symbolic link on Bookworm).
/// </summary>
/// <returns>An instance of the <see cref="ProcessSettings"/> class</returns>
public static ProcessSettings CreateForStillAndStderr()
=> IsRpicamAppsInstalled() ? CreateForRpicamstillAndStderr() : CreateForLibcamerastillAndStderr();
Comment on lines +159 to +160

/// <summary>
/// Creates a ProcessSettings instance for capturing still pictures using the libcamera/rpicam stack.
/// The new rpicam-still application is used when available, otherwise the legacy libcamera-still name
/// is used (which remains available as a symbolic link on Bookworm).
/// </summary>
/// <returns>An instance of the <see cref="ProcessSettings"/> class</returns>
public static ProcessSettings CreateForStill()
=> IsRpicamAppsInstalled() ? CreateForRpicamstill() : CreateForLibcamerastill();
Comment on lines +168 to +169

/// <summary>
/// Creates a ProcessSettings instance for capturing video streams using the libcamera/rpicam stack.
/// The new rpicam-vid application is used when available, otherwise the legacy libcamera-vid name
/// is used (which remains available as a symbolic link on Bookworm).
/// </summary>
/// <returns>An instance of the <see cref="ProcessSettings"/> class</returns>
public static ProcessSettings CreateForVid()
=> IsRpicamAppsInstalled() ? CreateForRpicamvid() : CreateForLibcameravid();
Comment on lines +177 to +178

private static bool IsApplicationOnPath(string fileName)
{
var pathVariable = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrEmpty(pathVariable))
{
return false;
}

foreach (var directory in pathVariable.Split(Path.PathSeparator))
{
if (string.IsNullOrWhiteSpace(directory))
{
continue;
}

try
{
if (File.Exists(Path.Combine(directory.Trim(), fileName)))
{
return true;
}
}
catch (Exception)
{
// Ignore invalid or inaccessible entries in the PATH variable
}
}

return false;
}
}
20 changes: 16 additions & 4 deletions src/devices/Camera/CameraInsights.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,25 @@ The `OS` versions are listed here: [Raspbian OS versions](https://www.raspberryp

The utilities to capture pictures or videos are fully [described in the documentation](https://www.raspberrypi.com/documentation/computers/camera_software.html). The two main utilities are the following:

| Feature | `raspicam` utilities | `libcamera` utilities |
| -------------- | -------------------- | --------------------- |
| Still pictures | `raspistill` | `libcamera-still` |
| video | `raspivid` | `libcamera-vid` |
| Feature | `raspicam` utilities | `libcamera` utilities | `rpicam` utilities |
| -------------- | -------------------- | --------------------- | ------------------ |
| Still pictures | `raspistill` | `libcamera-still` | `rpicam-still` |
| video | `raspivid` | `libcamera-vid` | `rpicam-vid` |

The command line described in the documentation is very similar for both the stacks.

### The `libcamera-*` to `rpicam-*` rename

Starting with `Raspberry Pi OS Bookworm`, the `libcamera-apps` have been [renamed to `rpicam-apps`](https://github.com/raspberrypi/rpicam-apps/blob/main/README.md) (for example `libcamera-still` becomes `rpicam-still` and `libcamera-vid` becomes `rpicam-vid`). Users are encouraged to adopt the new application names as soon as possible.

For backward compatibility, the older `libcamera-*` names are still provided on `Bookworm` as symbolic links pointing to the new `rpicam-*` executables, so existing code keeps working.

The `ProcessSettingsFactory` exposes dedicated methods for each naming:

- `CreateForLibcamerastill` / `CreateForLibcameravid` always use the legacy `libcamera-*` names.
- `CreateForRpicamstill` / `CreateForRpicamvid` always use the new `rpicam-*` names.
- `CreateForStill` / `CreateForVid` (and the related `*AndStderr` variants) automatically pick the `rpicam-*` applications when they are available on the system path, falling back to the `libcamera-*` names otherwise. These are the recommended methods because they keep working across all the supported OS releases.

From `Bullseye` on, the `raspi-config` app allows to re-enable or disable the legacy camera stack.

```bash
Expand Down
8 changes: 5 additions & 3 deletions src/devices/Camera/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,11 @@ Once the `ProcessRunner` has been created and the command line has been configur
The `ProcessSettingsFactory` exposes a few methods to prepare an instance of the `ProcessSettings` class with the correct application name.

```csharp
var processSettings = ProcessSettingsFactory.CreateForLibcamerastill();
var processSettings = ProcessSettingsFactory.CreateForStill();
```

> **The `libcamera-*` applications have been renamed to `rpicam-*`** starting with `Raspberry Pi OS Bookworm`. The `CreateForStill`, `CreateForVid` and `CreateForStillAndStderr` methods automatically select the new `rpicam-*` applications when they are installed, falling back to the legacy `libcamera-*` names otherwise. If you want to force a specific naming, use `CreateForRpicamstill`/`CreateForRpicamvid` or `CreateForLibcamerastill`/`CreateForLibcameravid` respectively.

In addition to the application name, the `ProcessSettings` has other two parameters:

- `BufferSize` is the size of the buffer used when copying data from `stdin` and the target stream
Expand Down Expand Up @@ -174,10 +176,10 @@ The `try/catch` block is necessary because the `Dispose` method also cancel the

### Listing the available cameras

This code is only supported with the `libcamera` stack and can be used with the `libcamera-still` or `libcamera-vid` applications, but remember that those two apps send the text output on `stderr` and not `stdout`.
This code is only supported with the `libcamera`/`rpicam` stack and can be used with the `libcamera-still`/`rpicam-still` or `libcamera-vid`/`rpicam-vid` applications, but remember that those apps send the text output on `stderr` and not `stdout`.

```csharp
var processSettings = ProcessSettingsFactory.CreateForLibcamerastillAndStderr();
var processSettings = ProcessSettingsFactory.CreateForStillAndStderr();
using var proc = new ProcessRunner(processSettings);
var text = await proc.ExecuteReadOutputAsStringAsync(string.Empty);
IEnumerable<CameraInfo> cameras = await CameraInfo.From(text);
Expand Down
28 changes: 23 additions & 5 deletions src/devices/Camera/samples/Camera.Samples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ internal class Program
private const string CmdStillLibcamera = "still-libcamera";
private const string CmdVideoLibcamera = "video-libcamera";
private const string CmdLapseLibcamera = "lapse-libcamera";
private const string CmdStillRpicam = "still-rpicam";
private const string CmdVideoRpicam = "video-rpicam";
private const string CmdLapseRpicam = "lapse-rpicam";
private const string CmdStill = "still";
private const string CmdVideo = "video";
private const string CmdLapse = "lapse";

private static async Task<int> Main(string[] args)
{
Expand All @@ -27,13 +33,19 @@ private static async Task<int> Main(string[] args)
var arg = args[0];
ProcessSettings? processSettings = arg switch
{
CmdList => ProcessSettingsFactory.CreateForLibcamerastillAndStderr(),
CmdList => ProcessSettingsFactory.CreateForStillAndStderr(),
CmdStillLegacy => ProcessSettingsFactory.CreateForRaspistill(),
CmdVideoLegacy => ProcessSettingsFactory.CreateForRaspivid(),
CmdLapseLegacy => ProcessSettingsFactory.CreateForRaspistill(),
CmdStillLibcamera => ProcessSettingsFactory.CreateForLibcamerastill(),
CmdVideoLibcamera => ProcessSettingsFactory.CreateForLibcameravid(),
CmdLapseLibcamera => ProcessSettingsFactory.CreateForLibcamerastill(),
CmdStillRpicam => ProcessSettingsFactory.CreateForRpicamstill(),
CmdVideoRpicam => ProcessSettingsFactory.CreateForRpicamvid(),
CmdLapseRpicam => ProcessSettingsFactory.CreateForRpicamstill(),
CmdStill => ProcessSettingsFactory.CreateForStill(),
CmdVideo => ProcessSettingsFactory.CreateForVid(),
CmdLapse => ProcessSettingsFactory.CreateForStill(),
_ => null,
};

Expand All @@ -55,23 +67,23 @@ private static async Task<int> Main(string[] args)
return 0;
}

if (arg == CmdStillLegacy || arg == CmdStillLibcamera)
if (arg == CmdStillLegacy || arg == CmdStillLibcamera || arg == CmdStillRpicam || arg == CmdStill)
{
var filename = await capture.CaptureStill();
Console.WriteLine($"Captured the picture: {filename}");

return 0;
}

if (arg == CmdVideoLegacy || arg == CmdVideoLibcamera)
if (arg == CmdVideoLegacy || arg == CmdVideoLibcamera || arg == CmdVideoRpicam || arg == CmdVideo)
{
var filename = await capture.CaptureVideo();
Console.WriteLine($"Captured the video: {filename}");

return 0;
}

if (arg == CmdLapseLegacy || arg == CmdLapseLibcamera)
if (arg == CmdLapseLegacy || arg == CmdLapseLibcamera || arg == CmdLapseRpicam || arg == CmdLapse)
{
await capture.CaptureTimelapse();
Console.WriteLine($"The time-lapse images have been saved to disk");
Expand All @@ -85,13 +97,19 @@ private static async Task<int> Main(string[] args)
private static int Usage()
{
Console.WriteLine($"Camera.Samples supports one the following arguments:");
Console.WriteLine($"{CmdList} print the cameras available on (libcamera stack only)");
Console.WriteLine($"{CmdList} print the cameras available on (libcamera/rpicam stack only)");
Console.WriteLine($"{CmdStillLegacy} capture a still using raspistill");
Console.WriteLine($"{CmdVideoLegacy} capture a 10s video using raspivid");
Console.WriteLine($"{CmdLapseLegacy} capture a time lapse (5 images for 10s) using raspistill");
Console.WriteLine($"{CmdStillLibcamera} capture a still using libcamera-still");
Console.WriteLine($"{CmdVideoLibcamera} capture a 10s video using libcamera-vid");
Console.WriteLine($"{CmdLapseLibcamera} capture a time lapse (5 images for 10s) using libcamera-still");
Console.WriteLine($"{CmdStillRpicam} capture a still using rpicam-still");
Console.WriteLine($"{CmdVideoRpicam} capture a 10s video using rpicam-vid");
Console.WriteLine($"{CmdLapseRpicam} capture a time lapse (5 images for 10s) using rpicam-still");
Console.WriteLine($"{CmdStill} capture a still auto-selecting rpicam-still or libcamera-still");
Console.WriteLine($"{CmdVideo} capture a 10s video auto-selecting rpicam-vid or libcamera-vid");
Console.WriteLine($"{CmdLapse} capture a time lapse auto-selecting rpicam-still or libcamera-still");
return -1;
}
}
70 changes: 70 additions & 0 deletions src/devices/Camera/tests/TestCamera/ProcessSettingsFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

using Iot.Device.Camera.Settings;

namespace TestCamera;

/// <summary>
/// Tests for the ProcessSettingsFactory class, in particular the migration
/// from the libcamera-* applications to the new rpicam-* applications.
/// </summary>
public class ProcessSettingsFactoryTests
{
/// <summary>
/// The rpicam-* constants must use the new application naming.
/// </summary>
[Fact]
public void RpicamConstantsUseTheNewNaming()
{
Assert.Equal("rpicam-still", ProcessSettingsFactory.RpicamStill);
Assert.Equal("rpicam-vid", ProcessSettingsFactory.RpicamVid);
}

/// <summary>
/// The rpicam factory methods must target the new application names.
/// </summary>
[Fact]
public void RpicamFactoryMethodsTargetTheNewApplications()
{
Assert.Equal(ProcessSettingsFactory.RpicamStill, ProcessSettingsFactory.CreateForRpicamstill().Filename);
Assert.Equal(ProcessSettingsFactory.RpicamVid, ProcessSettingsFactory.CreateForRpicamvid().Filename);

var stderr = ProcessSettingsFactory.CreateForRpicamstillAndStderr();
Assert.Equal(ProcessSettingsFactory.RpicamStill, stderr.Filename);
Assert.True(stderr.CaptureStderrInsteadOfStdout);
}

/// <summary>
/// When the rpicam-apps are installed, detection must report them as available.
/// </summary>
[Fact]
public void IsRpicamAppsInstalledDetectsTheNewApplications()
{
Assert.True(ProcessSettingsFactory.IsRpicamAppsInstalled(
name => name == ProcessSettingsFactory.RpicamStill));
Assert.True(ProcessSettingsFactory.IsRpicamAppsInstalled(
name => name == ProcessSettingsFactory.RpicamVid));
Assert.False(ProcessSettingsFactory.IsRpicamAppsInstalled(_ => false));
}

/// <summary>
/// The auto-detecting factory methods must prefer the rpicam-* applications
/// when they are available, otherwise fall back to the libcamera-* names.
/// </summary>
[Fact]
public void AutoFactoryMethodsPreferRpicamWhenAvailable()
{
// The auto methods rely on detection through the system PATH. The
// test agents do not ship the rpicam-apps, so the libcamera-* names
// are expected as the backward compatible fallback.
Assert.Equal(ProcessSettingsFactory.LibcameraStill, ProcessSettingsFactory.CreateForStill().Filename);
Assert.Equal(ProcessSettingsFactory.LibcameraVid, ProcessSettingsFactory.CreateForVid().Filename);

var stderr = ProcessSettingsFactory.CreateForStillAndStderr();
Assert.Equal(ProcessSettingsFactory.LibcameraStill, stderr.Filename);
Assert.True(stderr.CaptureStderrInsteadOfStdout);
}
Comment on lines +58 to +69
}
Loading