Skip to content

Commit 1b5bac2

Browse files
Segergrenclaude
andcommitted
feat: add automatic resource lifecycle management
Add ref counting for encoders so they auto-dispose when no outputs reference them. Centralize source/output tracking in Obs class with AutoDispose flag. Track audio encoders by track index to properly detach when replaced. Add GameCapture hook state tracking with Hooked/Unhooked events. Improve Scene disposal to clean up channel assignments and owned items. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d209866 commit 1b5bac2

8 files changed

Lines changed: 418 additions & 16 deletions

File tree

src/ObsKit.NET/Encoders/AudioEncoder.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ namespace ObsKit.NET.Encoders;
66

77
/// <summary>
88
/// Represents an OBS audio encoder (obs_encoder_t).
9+
/// Supports ref counting for automatic disposal when no outputs reference it.
910
/// </summary>
1011
public sealed class AudioEncoder : ObsObject
1112
{
13+
private int _refCount = 0;
14+
private readonly object _refLock = new();
1215
/// <summary>Known audio encoder type IDs.</summary>
1316
public static class Types
1417
{
@@ -156,6 +159,53 @@ internal void SetAudio(AudioHandle audio)
156159
ObsEncoder.obs_encoder_set_audio(Handle, audio);
157160
}
158161

162+
/// <summary>
163+
/// Attaches this encoder to an output, incrementing the ref count.
164+
/// </summary>
165+
internal void Attach()
166+
{
167+
lock (_refLock)
168+
{
169+
_refCount++;
170+
}
171+
}
172+
173+
/// <summary>
174+
/// Detaches this encoder from an output, decrementing the ref count.
175+
/// When ref count reaches 0 and AutoDispose is enabled, the encoder is disposed.
176+
/// </summary>
177+
internal void Detach()
178+
{
179+
bool shouldDispose = false;
180+
lock (_refLock)
181+
{
182+
_refCount--;
183+
if (_refCount <= 0 && Obs.AutoDispose)
184+
{
185+
shouldDispose = true;
186+
}
187+
}
188+
189+
if (shouldDispose)
190+
{
191+
Dispose();
192+
}
193+
}
194+
195+
/// <summary>
196+
/// Gets the current reference count (number of outputs using this encoder).
197+
/// </summary>
198+
public int RefCount
199+
{
200+
get
201+
{
202+
lock (_refLock)
203+
{
204+
return _refCount;
205+
}
206+
}
207+
}
208+
159209
/// <inheritdoc/>
160210
protected override void ReleaseHandle(nint handle)
161211
{

src/ObsKit.NET/Encoders/VideoEncoder.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ public enum RateControl
2121

2222
/// <summary>
2323
/// Represents an OBS video encoder (obs_encoder_t).
24+
/// Supports ref counting for automatic disposal when no outputs reference it.
2425
/// </summary>
2526
public sealed class VideoEncoder : ObsObject
2627
{
28+
private int _refCount = 0;
29+
private readonly object _refLock = new();
2730
/// <summary>Known video encoder type IDs.</summary>
2831
public static class Types
2932
{
@@ -259,6 +262,53 @@ internal void SetVideo(VideoHandle video)
259262
ObsEncoder.obs_encoder_set_video(Handle, video);
260263
}
261264

265+
/// <summary>
266+
/// Attaches this encoder to an output, incrementing the ref count.
267+
/// </summary>
268+
internal void Attach()
269+
{
270+
lock (_refLock)
271+
{
272+
_refCount++;
273+
}
274+
}
275+
276+
/// <summary>
277+
/// Detaches this encoder from an output, decrementing the ref count.
278+
/// When ref count reaches 0 and AutoDispose is enabled, the encoder is disposed.
279+
/// </summary>
280+
internal void Detach()
281+
{
282+
bool shouldDispose = false;
283+
lock (_refLock)
284+
{
285+
_refCount--;
286+
if (_refCount <= 0 && Obs.AutoDispose)
287+
{
288+
shouldDispose = true;
289+
}
290+
}
291+
292+
if (shouldDispose)
293+
{
294+
Dispose();
295+
}
296+
}
297+
298+
/// <summary>
299+
/// Gets the current reference count (number of outputs using this encoder).
300+
/// </summary>
301+
public int RefCount
302+
{
303+
get
304+
{
305+
lock (_refLock)
306+
{
307+
return _refCount;
308+
}
309+
}
310+
}
311+
262312
/// <inheritdoc/>
263313
protected override void ReleaseHandle(nint handle)
264314
{

src/ObsKit.NET/Obs.cs

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using ObsKit.NET.Core;
2+
using ObsKit.NET.Encoders;
23
using ObsKit.NET.Exceptions;
34
using ObsKit.NET.Native;
45
using ObsKit.NET.Native.Interop;
56
using ObsKit.NET.Native.Types;
7+
using ObsKit.NET.Outputs;
68
using ObsKit.NET.Scenes;
79
using ObsKit.NET.Sources;
810

@@ -17,11 +19,31 @@ public static class Obs
1719
private static ObsContext? _context;
1820
private static readonly object _lock = new();
1921

22+
// Tracking for auto-management
23+
private static readonly Dictionary<uint, Source> _channelSources = new();
24+
private static readonly List<Output> _managedOutputs = new();
25+
2026
/// <summary>
2127
/// Gets whether OBS is currently initialized.
2228
/// </summary>
2329
public static bool IsInitialized => ObsCore.obs_initialized();
2430

31+
/// <summary>
32+
/// Gets or sets whether to automatically dispose sources and outputs on Shutdown.
33+
/// Default is true.
34+
/// </summary>
35+
public static bool AutoDispose { get; set; } = true;
36+
37+
/// <summary>
38+
/// Gets all sources currently assigned to output channels.
39+
/// </summary>
40+
public static IReadOnlyDictionary<uint, Source> ChannelSources => _channelSources;
41+
42+
/// <summary>
43+
/// Gets all outputs being managed.
44+
/// </summary>
45+
public static IReadOnlyList<Output> ManagedOutputs => _managedOutputs;
46+
2547
/// <summary>
2648
/// Gets the OBS version string.
2749
/// </summary>
@@ -118,6 +140,7 @@ public static ObsContext Initialize(Action<ObsConfiguration>? configure)
118140

119141
/// <summary>
120142
/// Shuts down OBS and releases all resources.
143+
/// Any remaining outputs will be stopped and sources will be disposed.
121144
/// </summary>
122145
public static void Shutdown()
123146
{
@@ -126,6 +149,30 @@ public static void Shutdown()
126149
if (_context == null)
127150
return;
128151

152+
// Stop any remaining managed outputs
153+
foreach (var output in _managedOutputs.ToList())
154+
{
155+
try
156+
{
157+
if (output.IsActive)
158+
output.Stop();
159+
}
160+
catch { /* Ignore errors during cleanup */ }
161+
}
162+
_managedOutputs.Clear();
163+
164+
// Dispose all channel sources
165+
foreach (var (channel, source) in _channelSources.ToList())
166+
{
167+
try
168+
{
169+
ObsCore.obs_set_output_source(channel, ObsSourceHandle.Null);
170+
source.Dispose();
171+
}
172+
catch { /* Ignore errors during cleanup */ }
173+
}
174+
_channelSources.Clear();
175+
129176
_context.Dispose();
130177
_context = null;
131178
}
@@ -154,30 +201,96 @@ public static void SetAudio(Action<AudioSettings> configure)
154201
}
155202

156203
/// <summary>
157-
/// Sets a source for an output channel. OBS uses channels 0-5 for different purposes:
204+
/// Sets a source for an output channel. OBS uses channels 0-63 for different purposes:
158205
/// Channel 0: Primary video source (scene/game capture)
159206
/// Channel 1: Secondary video (display capture fallback)
160-
/// Channels 2-5: Audio sources (microphone, desktop audio, etc.)
207+
/// Channels 2+: Audio sources (microphone, desktop audio, etc.)
161208
/// </summary>
162-
/// <param name="channel">The output channel (0-5).</param>
209+
/// <param name="channel">The output channel (0-63).</param>
163210
/// <param name="source">The source to assign, or null to clear the channel.</param>
164211
public static void SetOutputSource(uint channel, Source? source)
165212
{
166213
ThrowIfNotInitialized();
214+
215+
lock (_lock)
216+
{
217+
// Remove existing source from tracking (but don't dispose - user may still want it)
218+
_channelSources.Remove(channel);
219+
220+
if (source != null)
221+
{
222+
_channelSources[channel] = source;
223+
source.AssignedChannel = channel;
224+
}
225+
}
226+
167227
var handle = source != null ? (ObsSourceHandle)(nint)source.NativeHandle : ObsSourceHandle.Null;
168228
ObsCore.obs_set_output_source(channel, handle);
169229
}
170230

171231
/// <summary>
172232
/// Clears a source from an output channel.
173233
/// </summary>
174-
/// <param name="channel">The output channel to clear (0-5).</param>
234+
/// <param name="channel">The output channel to clear (0-63).</param>
175235
public static void ClearOutputSource(uint channel)
176236
{
177237
ThrowIfNotInitialized();
238+
239+
lock (_lock)
240+
{
241+
if (_channelSources.TryGetValue(channel, out var source))
242+
{
243+
source.AssignedChannel = null;
244+
_channelSources.Remove(channel);
245+
}
246+
}
247+
178248
ObsCore.obs_set_output_source(channel, ObsSourceHandle.Null);
179249
}
180250

251+
/// <summary>
252+
/// Adds an output to be managed. Managed outputs are tracked and can be auto-disposed on Shutdown.
253+
/// </summary>
254+
/// <typeparam name="T">The output type.</typeparam>
255+
/// <param name="output">The output to manage.</param>
256+
/// <returns>The same output for chaining.</returns>
257+
public static T AddOutput<T>(T output) where T : Output
258+
{
259+
lock (_lock)
260+
{
261+
if (!_managedOutputs.Contains(output))
262+
{
263+
_managedOutputs.Add(output);
264+
}
265+
}
266+
return output;
267+
}
268+
269+
/// <summary>
270+
/// Called when an output is stopped to remove it from tracking.
271+
/// </summary>
272+
internal static void OnOutputStopped(Output output)
273+
{
274+
lock (_lock)
275+
{
276+
_managedOutputs.Remove(output);
277+
}
278+
}
279+
280+
/// <summary>
281+
/// Called when a source is disposed to remove it from channel tracking.
282+
/// </summary>
283+
internal static void OnSourceDisposed(Source source)
284+
{
285+
lock (_lock)
286+
{
287+
if (source.AssignedChannel.HasValue)
288+
{
289+
_channelSources.Remove(source.AssignedChannel.Value);
290+
}
291+
}
292+
}
293+
181294
/// <summary>
182295
/// Called by ObsContext when it's disposed directly (not through Obs.Shutdown).
183296
/// </summary>

0 commit comments

Comments
 (0)