Skip to content

Commit 63dd733

Browse files
committed
feat: add LoadModulesBeforeVideo function, update readme, fix repository url
1 parent ec93f56 commit 63dd733

8 files changed

Lines changed: 232 additions & 419 deletions

File tree

README.md

Lines changed: 56 additions & 404 deletions
Large diffs are not rendered by default.

src/ObsKit.NET/Core/ObsConfiguration.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public sealed class ObsConfiguration
1515
internal List<(string Bin, string Data)> ModulePaths { get; } = [];
1616
internal Action<ObsLogLevel, string>? LogHandler { get; private set; }
1717
internal HashSet<string> ExcludedModules { get; } = new(StringComparer.OrdinalIgnoreCase);
18+
internal bool LoadModulesBeforeVideo { get; private set; } = false;
1819

1920
/// <summary>
2021
/// Sets the locale for OBS (e.g., "en-US").
@@ -138,6 +139,18 @@ public ObsConfiguration ForHeadlessOperation()
138139
ExcludeWebSocket();
139140
return this;
140141
}
142+
143+
/// <summary>
144+
/// Loads modules before initializing video/audio subsystems.
145+
/// This is NOT the recommended order per OBS documentation, but may be required
146+
/// for DXGI Desktop Duplication to work correctly in some configurations
147+
/// (particularly when COM is initialized in STA mode by the host application).
148+
/// </summary>
149+
public ObsConfiguration WithModulesLoadedFirst()
150+
{
151+
LoadModulesBeforeVideo = true;
152+
return this;
153+
}
141154
}
142155

143156
/// <summary>
@@ -252,6 +265,16 @@ public VideoSettings Fps(uint numerator, uint denominator = 1)
252265
return this;
253266
}
254267

268+
/// <summary>
269+
/// Sets the GPU adapter index to use.
270+
/// </summary>
271+
/// <param name="adapterIndex">The adapter index (0 = default).</param>
272+
public VideoSettings WithAdapter(uint adapterIndex)
273+
{
274+
Adapter = adapterIndex;
275+
return this;
276+
}
277+
255278
/// <summary>
256279
/// Gets the default graphics module for the current platform.
257280
/// </summary>

src/ObsKit.NET/ObsContext.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,20 @@ private void Initialize()
7575
ObsCore.obs_add_module_path(binPath, dataPath);
7676
}
7777

78-
// Reset video
79-
ResetVideo(shutdownOnFailure: true);
80-
81-
// Reset audio
82-
ResetAudio(shutdownOnFailure: true);
83-
84-
// Load modules
85-
LoadModules();
78+
if (_config.LoadModulesBeforeVideo)
79+
{
80+
// Non-standard order: Load modules first (may help with DXGI)
81+
LoadModules();
82+
ResetVideo(shutdownOnFailure: true);
83+
ResetAudio(shutdownOnFailure: true);
84+
}
85+
else
86+
{
87+
// Standard order per OBS documentation
88+
ResetVideo(shutdownOnFailure: true);
89+
ResetAudio(shutdownOnFailure: true);
90+
LoadModules();
91+
}
8692
}
8793

8894
private void LoadModules()

src/ObsKit.NET/ObsKit.NET.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
99
<PackageId>ObsKit.NET</PackageId>
1010
<PackageReadmeFile>README.md</PackageReadmeFile>
11-
<PackageProjectUrl>https://github.com/Segra/ObsKit.NET</PackageProjectUrl>
12-
<RepositoryUrl>https://github.com/Segra/ObsKit.NET</RepositoryUrl>
11+
<PackageProjectUrl>https://github.com/Segergren/ObsKit.NET</PackageProjectUrl>
12+
<RepositoryUrl>https://github.com/Segergren/ObsKit.NET</RepositoryUrl>
1313
<RepositoryType>git</RepositoryType>
1414
</PropertyGroup>
1515

src/ObsKit.NET/Platform/IPlatformServices.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ public sealed class MonitorInfo
4646
/// </summary>
4747
public string DeviceName { get; init; } = string.Empty;
4848

49+
/// <summary>
50+
/// Gets the full device interface ID for OBS monitor capture.
51+
/// This is the preferred ID for DXGI/WGC capture methods.
52+
/// </summary>
53+
public string DeviceId { get; init; } = string.Empty;
54+
4955
/// <summary>
5056
/// Gets the monitor handle.
5157
/// </summary>

src/ObsKit.NET/Platform/Windows/Interop/User32.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,20 @@ internal static partial byte EnumDisplayMonitors(
8484

8585
#endregion
8686

87+
#region Display Device Enumeration
88+
89+
[LibraryImport(Lib, EntryPoint = "EnumDisplayDevicesA", SetLastError = true)]
90+
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
91+
internal static partial byte EnumDisplayDevices(
92+
nint lpDevice,
93+
uint iDevNum,
94+
nint lpDisplayDevice,
95+
uint dwFlags);
96+
97+
internal const uint EDD_GET_DEVICE_INTERFACE_NAME = 0x00000001;
98+
99+
#endregion
100+
87101
#region Structures
88102

89103
[StructLayout(LayoutKind.Sequential)]
@@ -114,6 +128,26 @@ internal struct MONITORINFOEX
114128
public readonly string GetDeviceName() => szDevice ?? string.Empty;
115129
}
116130

131+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
132+
internal struct DISPLAY_DEVICE
133+
{
134+
public int cb;
135+
136+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
137+
public string DeviceName;
138+
139+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
140+
public string DeviceString;
141+
142+
public uint StateFlags;
143+
144+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
145+
public string DeviceID;
146+
147+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
148+
public string DeviceKey;
149+
}
150+
117151
#endregion
118152
}
119153

src/ObsKit.NET/Platform/Windows/WindowsPlatform.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ internal sealed class WindowsPlatform : IPlatformServices
1212
{
1313
public IReadOnlyList<MonitorInfo> GetMonitors()
1414
{
15+
// First, build a map of device name to device interface ID using EnumDisplayDevices
16+
var deviceIdMap = BuildDeviceIdMap();
17+
1518
var monitors = new List<MonitorInfo>();
1619
int index = 0;
1720

@@ -29,11 +32,15 @@ public IReadOnlyList<MonitorInfo> GetMonitors()
2932
info = Marshal.PtrToStructure<User32.MONITORINFOEX>(infoPtr);
3033
var deviceName = info.GetDeviceName();
3134

35+
// Get the full device interface ID for OBS monitor capture
36+
deviceIdMap.TryGetValue(deviceName, out var deviceId);
37+
3238
monitors.Add(new MonitorInfo
3339
{
3440
Index = index++,
3541
Handle = hMonitor,
3642
DeviceName = deviceName,
43+
DeviceId = deviceId ?? deviceName, // Fall back to device name if not found
3744
Name = deviceName,
3845
X = info.rcMonitor.Left,
3946
Y = info.rcMonitor.Top,
@@ -56,6 +63,67 @@ public IReadOnlyList<MonitorInfo> GetMonitors()
5663
return monitors;
5764
}
5865

66+
/// <summary>
67+
/// Builds a map of device names (e.g., \\.\DISPLAY1) to device interface IDs
68+
/// using EnumDisplayDevices with EDD_GET_DEVICE_INTERFACE_NAME flag.
69+
/// </summary>
70+
private static Dictionary<string, string> BuildDeviceIdMap()
71+
{
72+
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
73+
74+
uint deviceIndex = 0;
75+
int displayDeviceSize = Marshal.SizeOf<User32.DISPLAY_DEVICE>();
76+
nint displayDevicePtr = Marshal.AllocHGlobal(displayDeviceSize);
77+
78+
try
79+
{
80+
while (true)
81+
{
82+
// Initialize DISPLAY_DEVICE structure
83+
var displayDevice = new User32.DISPLAY_DEVICE { cb = displayDeviceSize };
84+
Marshal.StructureToPtr(displayDevice, displayDevicePtr, false);
85+
86+
// Get display adapter info
87+
if (User32.EnumDisplayDevices(0, deviceIndex, displayDevicePtr, 0) == 0)
88+
break;
89+
90+
displayDevice = Marshal.PtrToStructure<User32.DISPLAY_DEVICE>(displayDevicePtr);
91+
var adapterName = displayDevice.DeviceName; // e.g., \\.\DISPLAY1
92+
93+
// Now get the monitor attached to this adapter with the device interface name flag
94+
var monitorDevice = new User32.DISPLAY_DEVICE { cb = displayDeviceSize };
95+
Marshal.StructureToPtr(monitorDevice, displayDevicePtr, false);
96+
97+
nint adapterNamePtr = Marshal.StringToHGlobalAnsi(adapterName);
98+
try
99+
{
100+
if (User32.EnumDisplayDevices(adapterNamePtr, 0, displayDevicePtr, User32.EDD_GET_DEVICE_INTERFACE_NAME) != 0)
101+
{
102+
monitorDevice = Marshal.PtrToStructure<User32.DISPLAY_DEVICE>(displayDevicePtr);
103+
var deviceId = monitorDevice.DeviceID; // Full device interface ID
104+
105+
if (!string.IsNullOrEmpty(deviceId))
106+
{
107+
map[adapterName] = deviceId;
108+
}
109+
}
110+
}
111+
finally
112+
{
113+
Marshal.FreeHGlobal(adapterNamePtr);
114+
}
115+
116+
deviceIndex++;
117+
}
118+
}
119+
finally
120+
{
121+
Marshal.FreeHGlobal(displayDevicePtr);
122+
}
123+
124+
return map;
125+
}
126+
59127
public MonitorInfo? GetPrimaryMonitor()
60128
{
61129
return GetMonitors().FirstOrDefault(m => m.IsPrimary);

src/ObsKit.NET/Sources/MonitorCapture.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,16 @@ private static Settings BuildInitialSettings(int monitorIndex, bool captureCurso
113113
var monitor = monitors.FirstOrDefault(m => m.Index == monitorIndex) ?? monitors.FirstOrDefault();
114114
if (monitor != null)
115115
{
116-
settings.Set("monitor_id", monitor.DeviceName);
116+
// Use DeviceId (full device interface ID like \\?\DISPLAY#SAM0FEC#...)
117+
// This is what OBS uses to match monitors for DXGI capture (duplicator_capture_info)
118+
settings.Set("monitor_id", monitor.DeviceId ?? monitor.DeviceName);
117119
}
120+
// Also set integer monitor index for GDI-based capture (monitor_capture_info)
121+
// OBS registers different source implementations with the same "monitor_capture" ID
122+
// depending on whether graphics_uses_d3d11 is true or false
123+
settings.Set("monitor", monitorIndex);
118124
settings.Set("capture_cursor", captureCursor);
119-
// Default to WGC (Windows Graphics Capture) to avoid DXGI errors
120-
// DXGI Desktop Duplication can fail with 0x887A0004 in certain scenarios
125+
// Default to WGC (Windows.Graphics.Capture) for better performance
121126
settings.Set("method", 2); // 0=auto, 1=DXGI, 2=WGC
122127
}
123128
else if (OperatingSystem.IsLinux())
@@ -165,8 +170,11 @@ public MonitorCapture SetMonitor(int monitorIndex)
165170
var monitor = monitors.FirstOrDefault(m => m.Index == monitorIndex) ?? monitors.FirstOrDefault();
166171
if (monitor != null)
167172
{
168-
s.Set("monitor_id", monitor.DeviceName);
173+
// For DXGI-based duplicator_capture_info (string monitor_id)
174+
s.Set("monitor_id", monitor.DeviceId ?? monitor.DeviceName);
169175
}
176+
// For GDI-based monitor_capture_info (integer monitor)
177+
s.Set("monitor", monitorIndex);
170178
}
171179
else if (OperatingSystem.IsMacOS())
172180
{
@@ -186,7 +194,10 @@ public MonitorCapture SetMonitor(MonitorInfo monitor)
186194
{
187195
if (OperatingSystem.IsWindows())
188196
{
189-
s.Set("monitor_id", monitor.DeviceName);
197+
// For DXGI-based duplicator_capture_info (string monitor_id)
198+
s.Set("monitor_id", monitor.DeviceId ?? monitor.DeviceName);
199+
// For GDI-based monitor_capture_info (integer monitor)
200+
s.Set("monitor", monitor.Index);
190201
}
191202
else if (OperatingSystem.IsMacOS())
192203
{
@@ -218,4 +229,17 @@ public MonitorCapture SetCaptureMethod(MonitorCaptureMethod method)
218229
}
219230
return this;
220231
}
232+
233+
/// <summary>
234+
/// Forces SDR output mode (Windows only). May help with HDR-related capture issues.
235+
/// </summary>
236+
/// <param name="forceSdr">Whether to force SDR mode.</param>
237+
public MonitorCapture SetForceSdr(bool forceSdr)
238+
{
239+
if (OperatingSystem.IsWindows())
240+
{
241+
Update(s => s.Set("force_sdr", forceSdr));
242+
}
243+
return this;
244+
}
221245
}

0 commit comments

Comments
 (0)