Skip to content

Commit d209866

Browse files
committed
feat: add streaming support
1 parent 5662902 commit d209866

8 files changed

Lines changed: 1302 additions & 0 deletions

File tree

ObsKit.NET.sln

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D20
1111
EndProject
1212
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObsKit.NET.Sample.ReplayBuffer", "samples\ObsKit.NET.Sample.ReplayBuffer\ObsKit.NET.Sample.ReplayBuffer.csproj", "{82EA2837-6A53-4DEE-B6EE-A5F2396E7B59}"
1313
EndProject
14+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObsKit.NET.Sample.Streaming", "samples\ObsKit.NET.Sample.Streaming\ObsKit.NET.Sample.Streaming.csproj", "{C3D4E5F6-A7B8-9012-CDEF-345678901234}"
15+
EndProject
1416
Global
1517
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1618
Debug|Any CPU = Debug|Any CPU
@@ -57,11 +59,25 @@ Global
5759
{82EA2837-6A53-4DEE-B6EE-A5F2396E7B59}.Release|x64.Build.0 = Release|Any CPU
5860
{82EA2837-6A53-4DEE-B6EE-A5F2396E7B59}.Release|x86.ActiveCfg = Release|Any CPU
5961
{82EA2837-6A53-4DEE-B6EE-A5F2396E7B59}.Release|x86.Build.0 = Release|Any CPU
62+
{C3D4E5F6-A7B8-9012-CDEF-345678901234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
63+
{C3D4E5F6-A7B8-9012-CDEF-345678901234}.Debug|Any CPU.Build.0 = Debug|Any CPU
64+
{C3D4E5F6-A7B8-9012-CDEF-345678901234}.Debug|x64.ActiveCfg = Debug|Any CPU
65+
{C3D4E5F6-A7B8-9012-CDEF-345678901234}.Debug|x64.Build.0 = Debug|Any CPU
66+
{C3D4E5F6-A7B8-9012-CDEF-345678901234}.Debug|x86.ActiveCfg = Debug|Any CPU
67+
{C3D4E5F6-A7B8-9012-CDEF-345678901234}.Debug|x86.Build.0 = Debug|Any CPU
68+
{C3D4E5F6-A7B8-9012-CDEF-345678901234}.Release|Any CPU.ActiveCfg = Release|Any CPU
69+
{C3D4E5F6-A7B8-9012-CDEF-345678901234}.Release|Any CPU.Build.0 = Release|Any CPU
70+
{C3D4E5F6-A7B8-9012-CDEF-345678901234}.Release|x64.ActiveCfg = Release|Any CPU
71+
{C3D4E5F6-A7B8-9012-CDEF-345678901234}.Release|x64.Build.0 = Release|Any CPU
72+
{C3D4E5F6-A7B8-9012-CDEF-345678901234}.Release|x86.ActiveCfg = Release|Any CPU
73+
{C3D4E5F6-A7B8-9012-CDEF-345678901234}.Release|x86.Build.0 = Release|Any CPU
6074
EndGlobalSection
6175
GlobalSection(SolutionProperties) = preSolution
6276
HideSolutionNode = FALSE
6377
EndGlobalSection
6478
GlobalSection(NestedProjects) = preSolution
6579
{82EA2837-6A53-4DEE-B6EE-A5F2396E7B59} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
80+
{C3D4E5F6-A7B8-9012-CDEF-345678901234} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
81+
{B2C3D4E5-F6A7-8901-BCDE-F23456789012} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
6682
EndGlobalSection
6783
EndGlobal

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ A modern .NET 9 wrapper for OBS Studio, providing a fluent C# API for video reco
66

77
- **Cross-Platform** - Windows, Linux, and macOS support
88
- **Fluent API** - Clean, chainable configuration
9+
- **Streaming** - Stream to Twitch, YouTube, Facebook, or custom RTMP servers
910
- **Recording** - Record video to MP4, MKV, FLV, and more
1011
- **Replay Buffer** - Keep a rolling buffer of the last N seconds
1112
- **Sources** - Monitor capture, window capture, game capture, images, media files
@@ -137,6 +138,48 @@ var encoder = VideoEncoder.CreateNvencHevc("Video", bitrate: 6000);
137138
var encoder = AudioEncoder.CreateAac("Audio", bitrate: 192);
138139
```
139140

141+
## Streaming
142+
143+
```csharp
144+
using ObsKit.NET.Outputs;
145+
using ObsKit.NET.Services;
146+
147+
// Stream to Twitch
148+
using var streaming = new StreamingOutput("My Stream")
149+
.ToTwitch("your_stream_key")
150+
.WithDefaultEncoders(videoBitrate: 4500, audioBitrate: 160);
151+
152+
// Stream to YouTube
153+
using var streaming = new StreamingOutput("My Stream")
154+
.ToYouTube("your_stream_key")
155+
.WithDefaultEncoders(videoBitrate: 4500, audioBitrate: 160);
156+
157+
// Stream to custom RTMP server
158+
using var streaming = new StreamingOutput("My Stream")
159+
.ToCustomServer("rtmp://live.example.com/app", "stream_key")
160+
.WithDefaultEncoders(videoBitrate: 4500, audioBitrate: 160);
161+
162+
// Full control with Service class
163+
using var service = Service.CreateCustom("rtmp://live.example.com/app", "stream_key");
164+
using var streaming = new StreamingOutput("My Stream")
165+
.WithService(service)
166+
.WithNvencEncoders(videoBitrate: 6000, audioBitrate: 160)
167+
.WithReconnect(enabled: true, retryDelaySec: 10, maxRetries: 20)
168+
.WithLowLatencyMode(enabled: true);
169+
170+
// Start streaming
171+
streaming.Start();
172+
173+
// Monitor stream status
174+
Console.WriteLine($"Streaming: {streaming.IsActive}");
175+
Console.WriteLine($"Bytes sent: {streaming.TotalBytes}");
176+
Console.WriteLine($"Frames dropped: {streaming.FramesDropped}");
177+
Console.WriteLine($"Congestion: {streaming.Congestion:P0}");
178+
179+
// Stop streaming
180+
streaming.Stop();
181+
```
182+
140183
## DPI Awareness (Windows)
141184

142185
When using DXGI Desktop Duplication for monitor capture on Windows, your application must be configured as **per-monitor DPI aware**.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<RootNamespace>ObsKit.NET.Sample.Streaming</RootNamespace>
6+
<ApplicationManifest>app.manifest</ApplicationManifest>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<ProjectReference Include="..\..\src\ObsKit.NET\ObsKit.NET.csproj" />
11+
</ItemGroup>
12+
13+
</Project>
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
using ObsKit.NET;
2+
using ObsKit.NET.Outputs;
3+
using ObsKit.NET.Sources;
4+
5+
Console.WriteLine("ObsKit.NET - Streaming Example");
6+
Console.WriteLine("==============================\n");
7+
8+
// OBS runtime should be set up alongside the application.
9+
// See README.md for instructions on setting up the OBS runtime.
10+
11+
var obsPath = AppContext.BaseDirectory;
12+
var dataPath = Path.Combine(obsPath, "data", "libobs");
13+
var pluginBinPath = Path.Combine(obsPath, "obs-plugins", "64bit");
14+
var pluginDataPath = Path.Combine(obsPath, "data", "obs-plugins", "%module%");
15+
16+
// Verify OBS runtime exists
17+
if (!File.Exists(Path.Combine(obsPath, "obs.dll")))
18+
{
19+
Console.WriteLine("ERROR: OBS runtime not found!");
20+
Console.WriteLine($"Expected obs.dll at: {obsPath}");
21+
Console.WriteLine("\nPlease set up the OBS runtime. See README.md for instructions.");
22+
return;
23+
}
24+
25+
// Initialize OBS
26+
using var obs = Obs.Initialize(config => config
27+
.WithDataPath(dataPath)
28+
.WithModulePath(pluginBinPath, pluginDataPath)
29+
.ForHeadlessOperation()
30+
.WithVideo(v => v.Resolution(1920, 1080).Fps(30)) // 30 FPS is common for streaming
31+
.WithAudio(a => a.WithSampleRate(48000)));
32+
33+
Console.WriteLine($"OBS {Obs.Version} initialized\n");
34+
35+
// Set up capture source (monitor capture in this example)
36+
var primaryMonitor = MonitorCapture.AvailableMonitors.FirstOrDefault(m => m.IsPrimary)
37+
?? MonitorCapture.AvailableMonitors.First();
38+
using var monitorSource = MonitorCapture.FromMonitor(primaryMonitor)
39+
.SetCaptureMethod(MonitorCaptureMethod.DesktopDuplication);
40+
41+
// Adjust video resolution to match monitor
42+
Obs.SetVideo(v => v.Resolution((uint)primaryMonitor.Width, (uint)primaryMonitor.Height).Fps(30));
43+
44+
// Set up audio sources
45+
using var audioInput = AudioInputCapture.FromDefault();
46+
using var audioOutput = AudioOutputCapture.FromDefault();
47+
48+
// Create a scene with monitor capture
49+
using var scene = Obs.Scenes.Create("Streaming Scene");
50+
scene.AddSource(monitorSource);
51+
scene.AddSource(audioInput);
52+
scene.AddSource(audioOutput);
53+
scene.SetAsProgram();
54+
55+
Console.WriteLine($"Scene created with {scene.ItemCount} source(s)\n");
56+
57+
// ========================================
58+
// STREAMING CONFIGURATION
59+
// ========================================
60+
// Replace these values with your actual stream key and server!
61+
62+
// Option 1: Stream to a custom RTMP server
63+
//const string rtmpServer = "rtmp://localhost/live"; // Your RTMP server URL
64+
//const string streamKey = "test-stream-key"; // Your stream key
65+
66+
// Option 2: Stream to Twitch (uncomment and set your stream key)
67+
const string twitchStreamKey = "live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
68+
69+
// Option 3: Stream to YouTube (uncomment and set your stream key)
70+
// const string youtubeStreamKey = "xxxx-xxxx-xxxx-xxxx-xxxx";
71+
72+
// Create streaming output
73+
using var streaming = new StreamingOutput("My Stream")
74+
// Configure destination - choose ONE of the following:
75+
76+
// Custom RTMP server:
77+
//.ToCustomServer(rtmpServer, streamKey)
78+
79+
// Or Twitch:
80+
.ToTwitch(twitchStreamKey)
81+
82+
// Or YouTube:
83+
// .ToYouTube(youtubeStreamKey)
84+
85+
// Or Facebook:
86+
// .ToFacebook(facebookStreamKey)
87+
88+
// Or use a Service directly for full control:
89+
// .WithService(Service.CreateCustom(rtmpServer, streamKey))
90+
91+
// Configure encoders
92+
// For streaming, lower bitrates are typically used (4500 kbps is common for 1080p)
93+
// This is the equivalent of setting the encoders above like
94+
// .WithVideoEncoder(VideoEncoder.CreateH264("Streaming Video", 4500, 1920, 1080, 30, "veryfast"))
95+
// .WithAudioEncoder(AudioEncoder.CreateAac("Streaming Audio", 160))
96+
.WithDefaultEncoders(videoBitrate: 4500, audioBitrate: 160, preset: "veryfast")
97+
98+
// Or use NVENC if you have an NVIDIA GPU:
99+
// .WithNvencEncoders(videoBitrate: 6000, audioBitrate: 160)
100+
101+
// Optional: Configure reconnection
102+
.WithReconnect(enabled: true, retryDelaySec: 10, maxRetries: 20)
103+
104+
// Optional: Enable low latency mode for reduced delay
105+
.WithLowLatencyMode(enabled: true);
106+
107+
Console.WriteLine("Streaming Configuration:");
108+
Console.WriteLine($" Server: {streaming.Url}");
109+
Console.WriteLine($" Can connect: {streaming.Service?.CanConnect}");
110+
Console.WriteLine();
111+
112+
// Connect to streaming events
113+
using var startedHandler = streaming.OnStarted(() =>
114+
{
115+
Console.WriteLine("[Event] Stream started successfully!");
116+
});
117+
118+
using var stoppedHandler = streaming.OnStopped(code =>
119+
{
120+
Console.WriteLine($"[Event] Stream stopped with code: {code}");
121+
});
122+
123+
using var reconnectingHandler = streaming.OnReconnecting(() =>
124+
{
125+
Console.WriteLine("[Event] Connection lost, attempting to reconnect...");
126+
});
127+
128+
using var reconnectedHandler = streaming.OnReconnected(() =>
129+
{
130+
Console.WriteLine("[Event] Reconnected successfully!");
131+
});
132+
133+
// Start streaming
134+
Console.WriteLine("Starting stream... Press any key to stop.\n");
135+
136+
if (!streaming.Start())
137+
{
138+
Console.WriteLine($"Failed to start stream: {streaming.LastError}");
139+
return;
140+
}
141+
142+
Console.WriteLine("Streaming in progress...\n");
143+
144+
// Display stats periodically
145+
var cts = new CancellationTokenSource();
146+
var statsTask = Task.Run(async () =>
147+
{
148+
while (!cts.Token.IsCancellationRequested)
149+
{
150+
await Task.Delay(2000, cts.Token).ConfigureAwait(false);
151+
if (streaming.IsActive)
152+
{
153+
var mbSent = streaming.TotalBytes / 1024.0 / 1024.0;
154+
Console.WriteLine($" Stats: {streaming.TotalFrames} frames, {mbSent:F2} MB sent, {streaming.FramesDropped} dropped, congestion: {streaming.Congestion:P0}, delay: {streaming.ActiveDelay}");
155+
}
156+
}
157+
}, cts.Token);
158+
159+
// Wait for user to stop
160+
Console.ReadKey(intercept: true);
161+
cts.Cancel();
162+
163+
// Stop streaming
164+
Console.WriteLine("\nStopping stream...");
165+
streaming.Stop();
166+
167+
Console.WriteLine("\nStream ended!");
168+
Console.WriteLine($" Total frames: {streaming.TotalFrames}");
169+
Console.WriteLine($" Total bytes: {streaming.TotalBytes:N0}");
170+
Console.WriteLine($" Frames dropped: {streaming.FramesDropped}");
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
3+
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
4+
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
5+
<security>
6+
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
7+
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
8+
</requestedPrivileges>
9+
</security>
10+
</trustInfo>
11+
12+
<application xmlns="urn:schemas-microsoft-com:asm.v3">
13+
<windowsSettings>
14+
<!-- Per-monitor DPI awareness V2 - required for DXGI output duplication -->
15+
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
16+
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
17+
</windowsSettings>
18+
</application>
19+
20+
</assembly>

0 commit comments

Comments
 (0)