Skip to content

Commit b2664f4

Browse files
committed
Add DirectShow camera rename commands
1 parent 3d28c3f commit b2664f4

4 files changed

Lines changed: 256 additions & 23 deletions

File tree

PTZControlConsole/CameraBackend.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
using System.IO;
44
using System.Linq;
55
using System.Runtime.InteropServices;
6+
using System.Runtime.Versioning;
67
using PTZControl.Uvc;
78

89
namespace PTZControlConsole;
910

1011
internal interface ICameraBackend
1112
{
1213
IReadOnlyList<CameraInfo> Enumerate();
14+
string GetDirectShowCameraName(string devicePath);
15+
void SetDirectShowCameraName(string devicePath, string friendlyName);
1316
(int min, int max, int step, int def) GetRange(string camera, CameraProperty property);
1417
int GetValue(string camera, CameraProperty property);
1518
void SetPanTiltZoom(string camera, int? pan = null, int? tilt = null, int? zoom = null);
@@ -37,10 +40,51 @@ public static ICameraBackend Create()
3740
}
3841
}
3942

43+
[SupportedOSPlatform("windows")]
4044
internal sealed class WindowsUvcCameraBackend : ICameraBackend
4145
{
4246
public IReadOnlyList<CameraInfo> Enumerate() => UvcCamera.Enumerate();
4347

48+
public string GetDirectShowCameraName(string devicePath)
49+
{
50+
if (string.IsNullOrWhiteSpace(devicePath))
51+
throw new ArgumentException("Device path is required.", nameof(devicePath));
52+
53+
using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(BuildDirectShowCameraRegistryPath(devicePath));
54+
return key?.GetValue("FriendlyName") as string
55+
?? throw new InvalidOperationException("DirectShow FriendlyName was not found for the selected camera.");
56+
}
57+
58+
public void SetDirectShowCameraName(string devicePath, string friendlyName)
59+
{
60+
if (string.IsNullOrWhiteSpace(devicePath))
61+
throw new ArgumentException("Device path is required.", nameof(devicePath));
62+
if (string.IsNullOrWhiteSpace(friendlyName))
63+
throw new ArgumentException("Friendly name is required.", nameof(friendlyName));
64+
65+
using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(BuildDirectShowCameraRegistryPath(devicePath), writable: true)
66+
?? throw new InvalidOperationException("DirectShow camera registry key was not found for the selected camera.");
67+
key.SetValue("FriendlyName", friendlyName, Microsoft.Win32.RegistryValueKind.String);
68+
}
69+
70+
private static string BuildDirectShowCameraRegistryPath(string devicePath)
71+
{
72+
var path = devicePath.Trim();
73+
const string monikerPrefix = "@device:pnp:";
74+
if (path.StartsWith(monikerPrefix, StringComparison.OrdinalIgnoreCase))
75+
path = path[monikerPrefix.Length..];
76+
77+
path = path.ToLowerInvariant()
78+
.Replace(@"\\?\usb", @"\##?#USB", StringComparison.OrdinalIgnoreCase)
79+
.Replace(@"\global", @"\#GLOBAL\Device Parameters", StringComparison.OrdinalIgnoreCase)
80+
.Replace(@"\{", @"\#{", StringComparison.OrdinalIgnoreCase);
81+
82+
if (!path.EndsWith(@"\device parameters", StringComparison.OrdinalIgnoreCase))
83+
path += @"\Device Parameters";
84+
85+
return @"SYSTEM\CurrentControlSet\Control\DeviceClasses\{65E8773D-8F56-11D0-A3B9-00A0C9223196}" + path;
86+
}
87+
4488
public (int min, int max, int step, int def) GetRange(string camera, CameraProperty property) =>
4589
UvcCamera.GetRange(camera, property);
4690

@@ -94,6 +138,12 @@ public IReadOnlyList<CameraInfo> Enumerate()
94138
.ToList();
95139
}
96140

141+
public string GetDirectShowCameraName(string devicePath) =>
142+
throw new NotSupportedException("DirectShow camera names are only available on Windows.");
143+
144+
public void SetDirectShowCameraName(string devicePath, string friendlyName) =>
145+
throw new NotSupportedException("DirectShow camera rename is only available on Windows.");
146+
97147
public (int min, int max, int step, int def) GetRange(string camera, CameraProperty property)
98148
{
99149
using var device = OpenCamera(camera);
@@ -278,6 +328,12 @@ internal sealed class UnsupportedCameraBackend(string message) : ICameraBackend
278328
{
279329
public IReadOnlyList<CameraInfo> Enumerate() => throw new NotSupportedException(message);
280330

331+
public string GetDirectShowCameraName(string devicePath) =>
332+
throw new NotSupportedException("DirectShow camera names are only available on Windows.");
333+
334+
public void SetDirectShowCameraName(string devicePath, string friendlyName) =>
335+
throw new NotSupportedException("DirectShow camera rename is only available on Windows.");
336+
281337
public (int min, int max, int step, int def) GetRange(string camera, CameraProperty property) =>
282338
throw new NotSupportedException(message);
283339

PTZControlConsole/Program.cs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.IO;
33
using System.Reflection;
4+
using System.Security;
45
using System.Runtime.Versioning;
56
using System.Text;
67
using System.Text.Json;
@@ -25,6 +26,8 @@ class Program
2526
typeof(GetCameraNameOptions),
2627
typeof(SetCameraNameOptions),
2728
typeof(ClearCameraNameOptions),
29+
typeof(GetDirectShowCameraNameOptions),
30+
typeof(SetDirectShowCameraNameOptions),
2831
typeof(SwapPresetNamesOptions),
2932
typeof(ConfigOptions),
3033
typeof(RestoreHomeOptions),
@@ -116,6 +119,8 @@ static int RunParsed(object parsed) =>
116119
GetCameraNameOptions options => GetCameraName(ToOptions(options)),
117120
SetCameraNameOptions options => SetCameraName(ToOptions(options)),
118121
ClearCameraNameOptions options => ClearCameraName(ToOptions(options)),
122+
GetDirectShowCameraNameOptions options => GetDirectShowCameraName(ToOptions(options)),
123+
SetDirectShowCameraNameOptions options => SetDirectShowCameraName(ToOptions(options), options.AcknowledgeWarning),
119124
SwapPresetNamesOptions options => SwapPresetNames(RequireSlot(options.SlotA, "--slot-a"), RequireSlot(options.SlotB, "--slot-b")),
120125
ConfigOptions options => RunConfig(ToOptions(options)),
121126
RestoreHomeOptions options => RestoreHome(ResolveCamera(ToOptions(options)), ParseTarget(options.TargetName)),
@@ -425,6 +430,48 @@ static int ClearCameraName(Options options)
425430
return Ok();
426431
}
427432

433+
static int GetDirectShowCameraName(Options options)
434+
{
435+
var camera = ResolveSingleCameraInfo(options, requireExplicitSelector: true);
436+
Console.WriteLine(CameraBackend.GetDirectShowCameraName(camera.MonikerString));
437+
return 0;
438+
}
439+
440+
static int SetDirectShowCameraName(Options options, bool acknowledgeWarning)
441+
{
442+
if (string.IsNullOrWhiteSpace(options.FriendlyName))
443+
throw new ArgumentException("set-directshow-camera-name requires --friendlyname.");
444+
445+
var camera = ResolveSingleCameraInfo(options, requireExplicitSelector: true);
446+
if (!acknowledgeWarning)
447+
RequireDirectShowRenameConfirmation();
448+
449+
try
450+
{
451+
CameraBackend.SetDirectShowCameraName(camera.MonikerString, options.FriendlyName);
452+
}
453+
catch (UnauthorizedAccessException ex)
454+
{
455+
throw new InvalidOperationException("Administrator rights are required to set the DirectShow camera name. Start the console as administrator or use an elevated shell.", ex);
456+
}
457+
catch (SecurityException ex)
458+
{
459+
throw new InvalidOperationException("Administrator rights are required to set the DirectShow camera name. Start the console as administrator or use an elevated shell.", ex);
460+
}
461+
462+
return Ok();
463+
}
464+
465+
static void RequireDirectShowRenameConfirmation()
466+
{
467+
Console.Error.WriteLine("Warning:");
468+
Console.Error.WriteLine(DirectShowRenameWarningText);
469+
Console.Error.Write("Type YES to continue: ");
470+
var answer = Console.ReadLine();
471+
if (!string.Equals(answer, "YES", StringComparison.Ordinal))
472+
throw new OperationCanceledException("DirectShow camera rename was not confirmed.");
473+
}
474+
428475
static int RunConfig(Options options)
429476
{
430477
if (options.ExportConfig && !string.IsNullOrWhiteSpace(options.ImportConfigPath))
@@ -724,6 +771,61 @@ static string ResolveCamera(Options options)
724771
return (null, null);
725772
}
726773

774+
static CameraInfo ResolveSingleCameraInfo(Options options, bool requireExplicitSelector)
775+
{
776+
var selectorCount =
777+
(!string.IsNullOrWhiteSpace(options.Camera) ? 1 : 0) +
778+
(!string.IsNullOrWhiteSpace(options.DevicePath) ? 1 : 0) +
779+
(options.Slot.HasValue ? 1 : 0);
780+
if (selectorCount > 1)
781+
throw new ArgumentException("Use only one camera selector: --camera, --device-path, or --slot.");
782+
if (requireExplicitSelector && selectorCount == 0)
783+
throw new ArgumentException("Use --camera, --device-path, or --slot to select the camera.");
784+
785+
var cameras = CameraBackend.Enumerate();
786+
if (cameras.Count == 0)
787+
throw new InvalidOperationException("No camera found.");
788+
789+
if (options.Slot.HasValue)
790+
{
791+
var slot = RequireSlot(options.Slot, "--slot");
792+
if (cameras.Count < slot)
793+
throw new InvalidOperationException($"Camera slot {slot} is not available. Found {cameras.Count} camera(s).");
794+
return RequireDevicePath(cameras[slot - 1]);
795+
}
796+
797+
if (!string.IsNullOrWhiteSpace(options.DevicePath))
798+
{
799+
var match = cameras
800+
.Where(camera => !string.IsNullOrWhiteSpace(camera.MonikerString) &&
801+
string.Equals(camera.MonikerString, options.DevicePath, StringComparison.OrdinalIgnoreCase))
802+
.ToList();
803+
if (match.Count == 1)
804+
return RequireDevicePath(match[0]);
805+
806+
return new CameraInfo { Name = options.DevicePath, MonikerString = options.DevicePath };
807+
}
808+
809+
if (!string.IsNullOrWhiteSpace(options.Camera))
810+
{
811+
var matches = cameras.Where(camera => CameraMatches(camera, options.Camera)).ToList();
812+
if (matches.Count == 0)
813+
throw new InvalidOperationException($"Camera '{options.Camera}' not found.");
814+
if (matches.Count > 1)
815+
throw new InvalidOperationException($"Camera selector '{options.Camera}' is ambiguous. Use --device-path instead.");
816+
return RequireDevicePath(matches[0]);
817+
}
818+
819+
return RequireDevicePath(cameras[0]);
820+
}
821+
822+
static CameraInfo RequireDevicePath(CameraInfo camera)
823+
{
824+
if (string.IsNullOrWhiteSpace(camera.MonikerString))
825+
throw new InvalidOperationException($"Camera '{camera.Name}' does not provide a device path.");
826+
return camera;
827+
}
828+
727829
static bool CameraMatches(CameraInfo camera, string query) =>
728830
camera.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
729831
(!string.IsNullOrWhiteSpace(camera.MonikerString) && camera.MonikerString.Contains(query, StringComparison.OrdinalIgnoreCase));
@@ -1160,6 +1262,21 @@ sealed class ClearCameraNameOptions
11601262
public int? Slot { get; set; }
11611263
}
11621264

1265+
[Verb("get-directshow-camera-name", HelpText = "Print the Windows DirectShow camera name.")]
1266+
sealed class GetDirectShowCameraNameOptions : CameraSelectionOptions
1267+
{
1268+
}
1269+
1270+
[Verb("set-directshow-camera-name", HelpText = "Set the Windows DirectShow camera name.")]
1271+
sealed class SetDirectShowCameraNameOptions : CameraSelectionOptions, IFriendlyNameOptions
1272+
{
1273+
[Option('n', "friendlyname", Required = true, HelpText = "DirectShow friendly name to write.")]
1274+
public string? FriendlyName { get; set; }
1275+
1276+
[Option("acknowledge-warning", HelpText = "Skip the interactive registry risk confirmation.")]
1277+
public bool AcknowledgeWarning { get; set; }
1278+
}
1279+
11631280
[Verb("swap-preset-names", HelpText = "Swap preset friendly names between two camera slots.")]
11641281
sealed class SwapPresetNamesOptions
11651282
{
@@ -1316,4 +1433,7 @@ sealed class Options
13161433
public int? X { get; set; }
13171434
public int? Y { get; set; }
13181435
}
1436+
1437+
private const string DirectShowRenameWarningText =
1438+
"This feature is provided \"as is\" without any warranty. The authors are not responsible for any damage to the system or the cameras. Use this feature at your own risk. It is recommended to create a backup of the registry path HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\DeviceClasses\\{65e8773d-8f56-11d0-a3b9-00a0c9223196} before using this feature, especially if you are not sure what you are doing.";
13191439
}

docs/generated/cli-help.md

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,33 @@ This file is generated by `PTZControlConsole docs --output docs/generated`.
77
```text
88
PTZControlConsole 1.0.0.0
99
10-
list-devices List available camera devices.
11-
cam-device-info Show camera device information and supported raw ranges.
12-
get-preset-name Print the friendly name of one preset.
13-
set-preset-name Store the friendly name of one preset.
14-
clear-preset-name Clear the friendly name of one preset.
15-
get-camera-name Print the friendly name of one camera slot.
16-
set-camera-name Store the friendly name of one camera slot.
17-
clear-camera-name Clear the friendly name of one camera slot.
18-
swap-preset-names Swap preset friendly names between two camera slots.
19-
config Export or import preset and camera friendly-name
20-
metadata.
21-
restore-home Restore the Logitech home position.
22-
restore-default Restore driver default values.
23-
restore-preset Restore a camera preset position.
24-
save-preset Save the current camera position as a preset.
25-
zoom-absolute Set an absolute zoom value.
26-
zoom-relative Change zoom by a relative delta.
27-
move-absolute Set absolute pan and/or tilt values.
28-
move-seek Experimentally seek absolute pan and/or tilt values using
29-
relative movement.
30-
move-relative Change pan and/or tilt by relative deltas.
31-
help Display more information on a specific command.
32-
version Display version information.
10+
list-devices List available camera devices.
11+
cam-device-info Show camera device information and supported raw
12+
ranges.
13+
get-preset-name Print the friendly name of one preset.
14+
set-preset-name Store the friendly name of one preset.
15+
clear-preset-name Clear the friendly name of one preset.
16+
get-camera-name Print the friendly name of one camera slot.
17+
set-camera-name Store the friendly name of one camera slot.
18+
clear-camera-name Clear the friendly name of one camera slot.
19+
get-directshow-camera-name Print the Windows DirectShow camera name.
20+
set-directshow-camera-name Set the Windows DirectShow camera name.
21+
swap-preset-names Swap preset friendly names between two camera
22+
slots.
23+
config Export or import preset and camera friendly-name
24+
metadata.
25+
restore-home Restore the Logitech home position.
26+
restore-default Restore driver default values.
27+
restore-preset Restore a camera preset position.
28+
save-preset Save the current camera position as a preset.
29+
zoom-absolute Set an absolute zoom value.
30+
zoom-relative Change zoom by a relative delta.
31+
move-absolute Set absolute pan and/or tilt values.
32+
move-seek Experimentally seek absolute pan and/or tilt
33+
values using relative movement.
34+
move-relative Change pan and/or tilt by relative deltas.
35+
help Display more information on a specific command.
36+
version Display version information.
3337
```
3438

3539
## list-devices
@@ -160,6 +164,44 @@ PTZControlConsole 1.0.0.0
160164
161165
```
162166

167+
## get-directshow-camera-name
168+
169+
```text
170+
PTZControlConsole 1.0.0.0
171+
172+
-c, --camera Camera device name fragment.
173+
174+
-d, --device-path Concrete camera device path.
175+
176+
-s, --slot PTZControl camera slot 1..3.
177+
178+
--help Display this help screen.
179+
180+
--version Display version information.
181+
182+
```
183+
184+
## set-directshow-camera-name
185+
186+
```text
187+
PTZControlConsole 1.0.0.0
188+
189+
-n, --friendlyname Required. DirectShow friendly name to write.
190+
191+
--acknowledge-warning Skip the interactive registry risk confirmation.
192+
193+
-c, --camera Camera device name fragment.
194+
195+
-d, --device-path Concrete camera device path.
196+
197+
-s, --slot PTZControl camera slot 1..3.
198+
199+
--help Display this help screen.
200+
201+
--version Display version information.
202+
203+
```
204+
163205
## swap-preset-names
164206

165207
```text

docs/syntax.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,21 @@ PTZControlConsole swap-preset-names --slot-a 1..3 --slot-b 1..3
8484
Friendly names are metadata used by PTZControl and automation tools. They do
8585
not rename the physical camera device.
8686

87+
## Windows DirectShow camera names
88+
89+
```text
90+
PTZControlConsole get-directshow-camera-name [-c|--camera "NamePart" | -d|--device-path "DevicePath" | -s|--slot 1..3]
91+
PTZControlConsole set-directshow-camera-name -n|--friendlyname "DirectShow Name" [-c|--camera "NamePart" | -d|--device-path "DevicePath" | -s|--slot 1..3] [--acknowledge-warning]
92+
```
93+
94+
These commands are Windows-only. They read or write the DirectShow
95+
`FriendlyName` registry value and do not rename the physical USB device or the
96+
Windows Device Manager device name.
97+
98+
`set-directshow-camera-name` shows an interactive registry risk confirmation
99+
before writing. Use `--acknowledge-warning` only for non-interactive scripts
100+
that already handle this risk intentionally.
101+
87102
## Metadata config transport
88103

89104
```text

0 commit comments

Comments
 (0)