A modern .NET 10 wrapper for OBS Studio, providing a fluent C# API for video recording, streaming, and replay buffer functionality.
usingObsKit.NET;usingObsKit.NET.Outputs;usingObsKit.NET.Sources;varobsPath=AppContext.BaseDirectory;// Initialize OBSusingvarobs=Obs.Initialize(config =>config.WithDataPath(Path.Combine(obsPath,"data","libobs")).WithModulePath(Path.Combine(obsPath,"obs-plugins","64bit"),Path.Combine(obsPath,"data","obs-plugins","%module%")).ForHeadlessOperation().WithVideo(v =>v.Resolution(1920,1080).Fps(60)).WithAudio(a =>a.WithSampleRate(48000)));Console.WriteLine($"OBS {Obs.Version} initialized");// Create a scene with monitor captureusingvarscene=Obs.Scenes.Create("My Scene");usingvarmonitor=MonitorCapture.FromPrimary();scene.AddSource(monitor);Obs.SetOutputSource(scene);// Obs.SetOutputSource(1, scene); // ...or assign to a specific channel (0 = program output; 1-63 hold additional global sources)// Set up and start recordingusingvarrecording=newRecordingOutput("My Recording").SetPath("output.mp4").SetFormat(RecordingFormat.Mp4).WithDefaultEncoders(videoBitrate:6000,audioBitrate:192);recording.Start();Console.WriteLine("Recording... Press any key to stop.");Console.ReadKey();Console.WriteLine($"Recorded {recording.TotalFrames} frames");recording.Stop();// with Obs.AutoDispose (default), Stop also disposes the outputGame/window capture only hooks its target while "showing". Keep a capture hooked before recording starts (instant first frames instead of a black lead-in):
using(game.KeepShowing())// hook stays active while the scope lives{// ... user hits record some time later; capture is already hookedrecording.Start();}// also: game.KeepActive() — full program-output activation semantics// Monitor captureusingvarmonitor=MonitorCapture.FromPrimary();usingvarmonitor=MonitorCapture.FromMonitor(1);// Window captureusingvarwindow=WindowCapture.FromWindow(WindowCapture.AvailableWindows[0]);// Game capture (Windows only) — optionally with the game's audio (Windows 10 2004+)usingvargame=newGameCapture("Game",GameCapture.CaptureMode.AnyFullscreen).SetCaptureAudio()// game audio without a separate source.SetCaptureOverlays()// include Steam/Discord overlays.SetHookRate(GameCapture.HookRate.Fast)// hook new games faster.SetRgb10A2ColorSpace(GameCapture.Rgb10A2ColorSpace.Pq2100);// HDR games// Hotkey mode: capture whatever window is in the foreground on demandusingvarhotkeyGame=newGameCapture("Game",GameCapture.CaptureMode.HotkeyForeground);hotkeyGame.CaptureForegroundWindow();// your app decides when (e.g. from its own global hotkey)// Image and mediausingvarimage=ImageSource.FromFile("logo.png");usingvarmedia=newMediaSource("Video","video.mp4").SetLooping(true);// Slideshow (image files or directories; navigate with NextMedia/PreviousMedia)usingvarslides=newSlideshowSource("Intermission",@"C:\art").SetSlideTime(TimeSpan.FromSeconds(5)).SetTransition(SlideshowSource.SlideTransition.Fade).SetLoop(true);// Webcam / video capture device (DirectShow on Windows, V4L2 on Linux, AVFoundation on macOS)foreach(vardinWebcamCapture.ListDevices())Console.WriteLine($" {d.Name} -> {d.DeviceId}");usingvarwebcam=WebcamCapture.FromDeviceName("BRIO")// partial name match??WebcamCapture.FromDefault();// first devicewebcam?.SetCustomResolution(3840,2160,30,videoFormat:"MJPEG");// optional: force 4K30// Audio capture (WASAPI on Windows, PulseAudio/PipeWire on Linux, Core Audio on macOS)foreach(vardinAudioInputCapture.ListDevices())// microphones (also: AudioOutputCapture.ListDevices())Console.WriteLine($" {d.Name} -> {d.DeviceId}");usingvarmic=AudioInputCapture.FromDefault();usingvardesktop=AudioOutputCapture.FromDefault();// Application audio capture (Windows 10 2004+, WASAPI process loopback)usingvardiscord=ApplicationAudioCapture.FromExecutable("Discord.exe");discord.Hooked+= s =>Console.WriteLine($"Capturing audio from {s.HookedExecutable}");// Text and solid colorusingvarlabel=newTextSource("Label","LIVE").SetFont("Arial",64).SetColor(0xFF0000FF);label.SetTextFromFile(@"C:\overlay\score.txt");// re-renders whenever the file changesusingvarbackground=newColorSource("Background",abgr:0xFF101010);// Browser overlay (requires the obs-browser plugin in the OBS runtime)if(BrowserSource.IsAvailable()){usingvaroverlay=newBrowserSource("Overlay","https://example.com/overlay",1920,1080).SetRerouteAudio();// control page audio like a sourceoverlay.SendJavascriptEvent("kill","{\"count\":3}");// window.addEventListener("kill", ...)overlay.Refresh();// reload, bypassing cache// Forward input for clickable overlays (coordinates in source pixels)overlay.SendMouseMove(640,360);overlay.SendMouseClick(640,360);// press...overlay.SendMouseClick(640,360,buttonUp:true);// ...and release}// Media playback control (any media-capable source)usingvarmedia=newMediaSource("Video","video.mp4");media.PauseMedia();media.MediaTime=TimeSpan.FromSeconds(30);Console.WriteLine($"{media.MediaState}: {media.MediaTime}/{media.MediaDuration}");Discover what a source's plugin exposes — for dynamic config UIs or to enumerate valid option values (device pickers, resolutions, FPS):
usingvarwebcam=newWebcamCapture("Cam");// Quick list of one property's options (display name + value)foreach(var(name,value)inwebcam.GetListPropertyItems("video_device_id"))Console.WriteLine($"{name} = {value}");// Full introspection: every property with type, state, ranges, and list itemsforeach(varpropinwebcam.GetProperties()){Console.WriteLine($"{prop.Name} ({prop.Type}): {prop.Description}");if(prop.IntRangeis(intmin,intmax,intstep))Console.WriteLine($" range {min}..{max} step {step}");foreach(variteminprop.ListItems)Console.WriteLine($" - {item.Name} = {item.StringValue??item.IntValue.ToString()}");}varwebcamItem=scene.AddSource(webcam);webcamItem.SetPosition(1440,810).SetBounds(ObsBoundsType.ScaleInner,480,270)// fit into a 480x270 box.SetScaleFilter(ObsScaleType.Lanczos)// high-quality downscale.SetBlendingMode(ObsBlendingType.Normal).SetShowTransition(TransitionTypes.Fade,TimeSpan.FromMilliseconds(250)).SetHideTransition(TransitionTypes.Fade,TimeSpan.FromMilliseconds(250));webcamItem.BoundsAlignment=ObsAlignment.TopLeft;// pin within the box (default: Center)webcamItem.IsVisible=false;// fades out instead of popping// Batch several transform changes into a single update signalusing(webcamItem.DeferUpdates()){webcamItem.SetPosition(0,0);webcamItem.SetBounds(ObsBoundsType.ScaleInner,960,540);}webcamItem.CropToBounds=true;// crop to the bounding box instead of overflowing// Free-form data saved with the item / source (not passed to the source plugin)usingvarpriv=webcamItem.GetPrivateSettings();// also source.GetPrivateSettings()priv.Set("my_app_tag","pinned");Group several items so they can be moved, scaled, and shown/hidden as one unit:
varoverlay=scene.AddGroup("Overlay");overlay.AddItem(webcamItem);// move existing items into the groupoverlay.AddItem(scene.AddSource(alertsBrowser));overlay.SetPosition(0,0).SetScale(0.5f,0.5f);// transforms the whole groupoverlay.IsVisible=false;// hides every member at onceforeach(varmemberinoverlay.GetGroupItems())Console.WriteLine(member.Source.Name);scene.GetGroup("Overlay")?.Ungroup();// disband, returning items to the sceneAnimate the program output between scenes. Assign a transition to an output channel, seed the starting scene, then animate to a new one:
usingObsKit.NET.Sources;usingvartransition=Transition.Fade();// or Transition.Cut(), .Slide(),// or new Transition(TransitionTypes.Wipe, "Wipe")transition.Set(introScene);// seed the current scene (no animation)Obs.SetOutputSource(0,transition);// the transition is now the program source// Cross-fade to gameplay over 300 mstransition.Start(gameplayScene,TimeSpan.FromMilliseconds(300));if(transition.IsTransitioning)transition.ForceStop();// snap to the destination immediately// Manual (scrubbed) transitions, e.g. driven by a slider:transition.Start(gameplayScene,TimeSpan.Zero,ObsTransitionMode.Manual);transition.SetManualTime(0.5f);// 0.0 = source A, 1.0 = source BTyped wrappers for the built-in OBS audio filters, with OBS defaults:
usingObsKit.NET.Filters;usingvarnoiseGate=newNoiseGateFilter().SetOpenThreshold(-42).SetCloseThreshold(-48).SetHoldTime(200);mic.AddFilter(noiseGate);usingvarsuppression=newNoiseSuppressFilter().SetMethod(NoiseSuppressFilter.SuppressionMethod.RnNoise);mic.AddFilter(suppression);// Also available: GainFilter, CompressorFilter, LimiterFilter, ExpanderFilter,// video filters (CropFilter, ColorCorrectionFilter, ChromaKeyFilter, SharpnessFilter,// ScaleFilter, RenderDelayFilter), and new Filter("any_filter_id", "Name") for everything else.// Inspect or reorder the filter chainforeach(varfinmic.GetFilters())Console.WriteLine(f.Name);// Bypass a filter without removing itsuppression.IsEnabled=false;// Apply the same processing chain to another sourcesecondMic.CopyFiltersFrom(mic);usingObsKit.NET.Audio;// Live level meter for UI VU meters (values in dB)usingvarmeter=newAudioMeter();meter.AttachSource(mic);meter.LevelsUpdated+=(m,levels)=>Console.WriteLine($"Peak: {levels.Peak[0]:F1} dB");// Mic sync alignment and stereo balancemic.AudioSyncOffset=TimeSpan.FromMilliseconds(120);mic.AudioBalance=0.5f;// Route monitored sources to a specific output deviceforeach(var(name,id)inObs.EnumerateAudioMonitoringDevices())Console.WriteLine($"{name}: {id}");Obs.SetAudioMonitoringDevice("default");// Let the user hear their mic through the monitoring devicemic.MonitoringType=ObsMonitoringType.MonitorAndOutput;// Set volume in dB directly (not clamped to unity, so it can apply gain)mic.VolumeDb=-6.0f;// Push-to-talk: mic stays muted unless its hotkey is held (200 ms release tail)mic.PushToTalkEnabled=true;mic.PushToTalkDelay=TimeSpan.FromMilliseconds(200);// Drive a source's volume from a UI slider with the same curve OBS usesusingvarfader=newVolumeFader();// cubic curve by defaultfader.AttachSource(mic);fader.Deflection=0.75f;// slider at 75% -> sets mic volumeConsole.WriteLine($"{fader.Db:F1} dB");Assign sources and encoders to audio tracks (1-6) without bitmask math:
mic.SetAudioTracks(1,2);// full mix + isolated mic trackdesktop.SetAudioTracks(1,3);// full mix + isolated desktop trackmic.SetAudioTrackEnabled(4);// add a single track// One audio encoder per track on the outputrecording.WithAudioEncoder(AudioEncoder.CreateAac("Mix",192,mixerIdx:0),track:0);recording.WithAudioEncoder(AudioEncoder.CreateAac("Mic",160,mixerIdx:1),track:1);// Full source screenshot (returns BGRA pixels, width, height — or null)varscreenshot=source.TakeScreenshot();// Cropped screenshot (only transfers the crop region from GPU)varcropped=source.TakeScreenshot(cropX:960,cropY:200,cropWidth:640,cropHeight:160);if(screenshot!=null){usingvarbmp=newBitmap((int)screenshot.Width,(int)screenshot.Height,(int)(screenshot.Width*4),PixelFormat.Format32bppArgb,Marshal.UnsafeAddrOfPinnedArrayElement(screenshot.Pixels,0));bmp.Save("screenshot.jpg",ImageFormat.Jpeg);}Subscribe to the live canvas output. OBS scales/converts each frame on the GPU to your requested format and resolution before invoking the callback on its video thread.
usingObsKit.NET;usingObsKit.NET.Native.Types;// Get every Nth frame at 480x270 BGRA (e.g. for a low-overhead preview).usingvarpreview=Obs.SubscribeRawVideo(VideoFormat.BGRA,width:480,height:270,callback:(inRawVideoFrameframe)=>{ReadOnlySpan<byte>pixels=frame.GetPackedPlane();// BGRA bytes (may have row padding — see GetLinesize(0))// ... encode to JPEG, push over IPC, etc. Don't block — this is OBS's video thread.},frameRateDivisor:6);// 60fps canvas → ~10fps callback// Dispose to stop receiving frames.Tap the mixed audio of any track — e.g. for waveform rendering, voice activity detection, or custom processing.
usingObsKit.NET.Audio;usingvartap=Obs.SubscribeRawAudio((inRawAudioFrameframe)=>{ReadOnlySpan<float>left=frame.GetFloatPlane(0);// planar float, one plane per channel// ... compute RMS, run VAD, etc. Don't block — this is OBS's audio thread.},track:1);// Or tap a single source before mixing (e.g. the microphone alone)usingvarmicTap=mic.SubscribeAudio((inRawAudioFrameframe,boolmuted)=>{// voice activity detection, custom noise processing, ...});// Dispose to stop receiving audio.usingvarrecording=newRecordingOutput("My Recording").SetPath("output.mp4").SetFormat(RecordingFormat.HybridMp4)// crash-resilient MP4 with chapter support (OBS 30.2+).WithBestEncoders(videoBitrate:12000);// NVENC -> AMF -> QuickSync -> x264// Typed stop info (disk full, encoder error, ...)recording.Stopped+=(_,e)=>{if(!e.IsSuccess)Console.WriteLine($"Recording stopped: {e.Code} ({e.LastError})");};recording.Start();// Chapter markers (Hybrid MP4/MOV only) — great for kill/goal/highlight bookmarksrecording.AddChapter("First blood");recording.AddChapter();// auto-named "Unnamed 2"recording.Stop();// Automatic + manual file splitting (file path is generated from the template)usingvarsplitRecording=newRecordingOutput("Split Recording").SetFormat(RecordingFormat.Mkv).WithFileSplitting(@"C:\Videos",maxTimeSeconds:30*60,extension:"mkv").WithDefaultEncoders();splitRecording.Start();splitRecording.SplitFile();// start a new file nowusingvarreplay=newReplayBuffer(maxSeconds:60).SetDirectory(@"C:\Videos\Replays").WithDefaultEncoders();replay.Start();// Awaitable save — completes when the file has finished writingstring?path=awaitreplay.SaveAsync();// Clear the buffered footage so the next save only contains new footage// (saving does not clear OBS's in-memory buffer — without this, two saves// close together contain overlapping footage)awaitreplay.ResetAsync();// Or event-drivenreplay.Saved+=(_,e)=>Console.WriteLine($"Replay saved to {e.Path}");replay.Save();Render the live canvas into a window of your app (WinForms, WPF via HwndHost, Avalonia native control host). OBS draws straight into the window's swap chain — no extra encoding, no CPU frame copies.
usingObsKit.NET.Video;usingvarpreview=newPreviewDisplay(panel.Handle,width:(uint)panel.ClientSize.Width,height:(uint)panel.ClientSize.Height);// Preview a single source or a secondary canvas instead of the main canvaspreview.Source=gameCapture;preview.Canvas=verticalCanvas;// see "Multiple Canvases"// Keep the surface in sync with the host control (sizes are physical pixels)panel.Resize+=(_,_)=>preview.Resize((uint)panel.ClientSize.Width,(uint)panel.ClientSize.Height);// Pause rendering while hiddenpreview.IsEnabled=false;Compose and record more than one view at once — e.g. a vertical 9:16 mix alongside the main horizontal recording:
usingObsKit.NET.Scenes;usingvarvertical=Canvas.Create("Vertical",1080,1920);usingvarverticalScene=vertical.CreateScene("Vertical Scene");verticalScene.AddSource(game);// same source, framed for 9:16vertical.SetScene(verticalScene);// Or reuse an existing layout: duplicate the main scene and move it overusingvarcopy=scene.Duplicate("Vertical Copy");vertical.MoveScene(copy);usingvarverticalRecording=newRecordingOutput("Vertical").SetPath("vertical.mp4").WithVideoEncoder(VideoEncoder.CreateBest("Vertical Video",8000),vertical,takeOwnership:true).WithAudioEncoder(AudioEncoder.CreateAac("Vertical Audio"),takeOwnership:true);verticalRecording.Start();// records simultaneously with the main outputRegister global hotkeys with OBS's hotkey system. libobs polls key state on its own background thread, so bound combinations fire system-wide — even while your app is not focused — with no OS hook code on your side:
usingObsKit.NET.Hotkeys;usingObsKit.NET.Native.Types;// App-level hotkey: save the replay buffer on Ctrl+Shift+F10usingvarsaveReplay=Obs.RegisterHotkey("save_replay","Save Replay",
pressed =>{if(pressed)replayBuffer.Save();});saveReplay.Bind(newObsKeyCombination(ObsKey.F10,ObsKeyModifiers.Control|ObsKeyModifiers.Shift));// Start/stop pair on one key: only the applicable half consumes the pressusingvarrecPair=Obs.RegisterHotkeyPair("start_rec","Start Recording","stop_rec","Stop Recording",onPrimary: pressed =>{if(pressed&&!recording.IsActive){recording.Start();returntrue;}returnfalse;},onSecondary: pressed =>{if(pressed&&recording.IsActive){recording.Stop();returntrue;}returnfalse;});recPair.BindPrimary(newObsKeyCombination(ObsKey.F9));recPair.BindSecondary(newObsKeyCombination(ObsKey.F9));// Source-scoped hotkey (auto-unregistered with the source)usingvarmicToggle=mic.RegisterHotkey("mic_toggle","Toggle Mic",
pressed =>{if(pressed)mic.IsMuted=!mic.IsMuted;});// Rebind libobs' built-in hotkeys (push-to-talk, mute, ...) by idforeach(varhkinObs.EnumerateHotkeys())if(hk.Name=="libobs.push-to-talk")Obs.BindHotkey(hk.Id,newObsKeyCombination(ObsKey.Mouse4));// Key conversions and display stringsvarkey=ObsKeys.FromVirtualKey(0x79);// Win32 VK_F10 -> ObsKey.F10varlabel=ObsKeys.GetDisplayString(combo);// "Ctrl + Shift + F10" (localized)varname=ObsKeys.ToName(key);// "OBS_KEY_F10" (for persistence)// Feed custom input events (optional - e.g. from a game overlay or remote control)Obs.InjectHotkeyEvent(newObsKeyCombination(ObsKey.F10,ObsKeyModifiers.Control),pressed:true);// Only fire presses you inject yourself (disable the background polling thread's presses)Obs.EnableHotkeyBackgroundPress(false);Look up any live object by name, or enumerate everything that currently exists. Returned wrappers hold their own reference — dispose them when done.
usingvargame=Source.GetByName("Game");// or Obs.Sources.Find("Game")usingvarbyUuid=Source.GetByUuid(uuid);// stable across renamesvareverything=Obs.Sources.ToList(includePrivate:true);usingvarrec=Output.GetByName("Recording");varoutputs=Output.GetAll();usingvarvenc=VideoEncoder.GetByName("Video");// also AudioEncoder.GetByNamevarencoders=VideoEncoder.GetAll();// also AudioEncoder.GetAllusingvarsvc=Service.GetByName("My Stream");usingvarcanvas=Canvas.GetByName("Vertical");// also Canvas.GetByUuid, Canvas.GetAllusingvarscene=canvas.FindScene("Vertical Scene");// scenes/sources scoped to one canvasvarscenes=canvas.GetScenes();// Duplicate a source (full copy of settings + filters)usingvarcopy=source.Duplicate("Copy",createPrivate:false);// Persist a source across sessions (settings, filters, volume, sync, monitoring, ...)using(varsaved=source.Save())saved.SaveToFileSafe("mic.json");usingvarrestored=Source.Load(Settings.FromJsonFileSafe("mic.json"));// Weak references: remember a source without keeping it aliveusingvarweak=source.GetWeakReference();usingvarstrong=weak.TryGetSource();// null once the source is gone// Unfiltered size and unversioned type iduintw=source.BaseWidth,h=source.BaseHeight;// size before crop/scale filtersstring?id=source.UnversionedTypeId;// "color_source" (id without the v3 version suffix)// Global stateboollive=Obs.IsVideoActive;// any recording/stream/vcam runningif(!Obs.IsAudioMonitoringAvailable){/* hide monitoring UI */}Obs.ResetAudioMonitoring();// recover after device loss// Which codecs an output type accepts (before picking encoders)varvcodecs=Output.GetSupportedVideoCodecs("ffmpeg_muxer");// ["h264", "hevc", "av1", ...]varacodecs=Output.GetSupportedAudioCodecs("rtmp_output");// ["aac"]// Best available hardware encoder (NVENC -> AMF -> QuickSync -> x264)varencoder=VideoEncoder.CreateBest("Video",bitrate:6000,preferHevc:true);// Video - x264 (CPU)varencoder=VideoEncoder.CreateX264("Video",bitrate:6000);// Video - NVENC (NVIDIA), AMF (AMD), QuickSync (Intel)varencoder=VideoEncoder.CreateNvencH264("Video",bitrate:6000);// also CreateNvencHevc, CreateNvencAv1varencoder=VideoEncoder.CreateAmfHevc("Video",bitrate:6000);// also CreateAmfH264, CreateAmfAv1varencoder=VideoEncoder.CreateQsvH264("Video",bitrate:6000);// also CreateQsvHevc, CreateQsvAv1// Audio - AAC, Opus, FLAC (lossless)varencoder=AudioEncoder.CreateAac("Audio",bitrate:192);varencoder=AudioEncoder.CreateFlac("Audio");// Record at a different resolution than the canvas, scaled on the GPUencoder.SetGpuScaledSize(1920,1080);// e.g. 1440p canvas -> 1080p recording// Record at a fraction of the canvas frame rateencoder.FrameRateDivisor=2;// 60 FPS canvas -> 30 FPS file// Prioritize quality in a region of the frame (encoders with ROI support)encoder.AddRegionOfInterest(newObsEncoderRoi{Top=300,Bottom=780,Left=640,Right=1280,Priority=0.75f});encoder.ClearRegionsOfInterest();// Per-encoder color space/range override (e.g. SDR stream while recording HDR)encoder.PreferredColorSpace=VideoColorspace.Srgb;encoder.PreferredRange=VideoRangeType.Partial;// Stats: frames encoded, and time spent paused (also on Output/AudioEncoder)uintframes=encoder.EncodedFrames;TimeSpanpaused=recording.PauseOffset;Query which encoders exist on the user's machine before creating one:
foreach(vareinEncoderInfo.GetVideoEncoders())Console.WriteLine($"{e.Id}: {e.DisplayName} [{e.Codec}, {e.Vendor}, HW={e.IsHardware}, HDR={e.SupportsHdr}]");if(EncoderInfo.IsAvailable(VideoEncoder.Types.NvencH264)){/* offer NVENC */}// Enumerate the valid options for an encoder property (presets, profiles, ...)foreach(var(name,value)inEncoderInfo.GetListPropertyItems(VideoEncoder.Types.NvencH264,"preset"))Console.WriteLine($"{name}: {value}");// HDR needs a 10-bit encoder (HEVC/AV1). Find the best match for the user's choice:varhdrEncoder=EncoderInfo.FindHdrCapable(preferredEncoderId:VideoEncoder.Types.NvencH264);// → same-vendor HEVC, then same-vendor AV1, then any HEVC/AV1, else null// Requires the OBS virtual camera driver (bundled with OBS Studio)if(VirtualCameraOutput.IsAvailable()){usingvarvirtualCam=newVirtualCameraOutput();virtualCam.Start();// canvas is now visible as a system camera}varstats=Obs.GetPerformanceStats();// equivalent to the OBS stats dockConsole.WriteLine(stats);// FPS, render time, lagged/skipped framesif(stats.EncodingLagRatio>0.05)Console.WriteLine("Encoder overloaded — lower the bitrate, resolution, or preset.");usingObsKit.NET.Outputs;usingObsKit.NET.Services;// Stream to Twitchusingvarstreaming=newStreamingOutput("My Stream").ToTwitch("your_stream_key").WithDefaultEncoders(videoBitrate:4500,audioBitrate:160);// Stream to YouTubeusingvarstreaming=newStreamingOutput("My Stream").ToYouTube("your_stream_key").WithDefaultEncoders(videoBitrate:4500,audioBitrate:160);// Stream to custom RTMP serverusingvarstreaming=newStreamingOutput("My Stream").ToCustomServer("rtmp://live.example.com/app","stream_key").WithDefaultEncoders(videoBitrate:4500,audioBitrate:160);// Stream over WHIP/WebRTC for sub-second latency (requires the obs-webrtc plugin; use Opus audio)usingvarstreaming=newStreamingOutput("My Stream").ToWhipEndpoint("https://example.com/whip",bearerToken:"token");// Full control with Service classusingvarservice=Service.CreateCustom("rtmp://live.example.com/app","stream_key");usingvarstreaming=newStreamingOutput("My Stream").WithService(service).WithNvencEncoders(videoBitrate:6000,audioBitrate:160).WithReconnect(enabled:true,retryDelaySec:10,maxRetries:20).WithLowLatencyMode(enabled:true);// Start streamingstreaming.Start();// Monitor stream statusConsole.WriteLine($"Streaming: {streaming.IsActive}");Console.WriteLine($"Bytes sent: {streaming.TotalBytes}");Console.WriteLine($"Frames dropped: {streaming.FramesDropped}");Console.WriteLine($"Congestion: {streaming.Congestion:P0}");// Embed closed captions (CEA-708) in the streamstreaming.SendCaption("Hello chat!",TimeSpan.FromSeconds(3));// Stop streamingstreaming.Stop();- Cross-Platform - Windows, Linux, and macOS support
- Fluent API - Clean, chainable configuration
- Streaming - Stream to Twitch, YouTube, Facebook, custom RTMP servers, or WHIP/WebRTC for sub-second latency
- Raw Data Taps - Zero-copy callbacks for video frames and audio samples (previews, waveforms, voice detection, custom processing)
- Recording - Record video to Hybrid MP4 (crash-resilient), MP4, MKV, FLV, and more, with chapter markers and file splitting
- Replay Buffer - Keep a rolling buffer of the last N seconds, with awaitable saves
- Preview Display - Render the live canvas (or one source) directly into your app's window
- Multiple Canvases - Record a second view simultaneously, e.g. a vertical 9:16 mix (OBS 31+)
- Sources - Monitor capture, window capture, game capture (with game audio), webcam, application audio, microphone/desktop audio, images, media files, text, solid color, browser overlays
- Filters - Noise gate, noise suppression, gain, compressor, limiter, expander, crop, color correction, chroma key, sharpness, scaling, render delay
- Scene Transitions - Animate the program output between scenes (fade, cut, slide, swipe, wipe, stinger), with auto or manual scrubbing
- Encoders - x264, NVENC, AMF, QuickSync, VideoToolbox (H.264/HEVC/AV1), AAC/Opus/FLAC audio, with runtime capability discovery
- Virtual Camera - Expose the canvas as a system camera device
- Audio Tooling - Per-track routing, live level meters, dB volume and curve-aware faders, sync offset, balance, monitoring device selection
- Property Introspection - Enumerate any source's configurable properties (types, ranges, option lists) to build dynamic config UIs or discover devices/resolutions
- Settings Introspection - Enumerate keys/types of any settings object, read defaults, JSON round-trips (with or without defaults), and crash-safe settings files (atomic save + backup-aware load)
- Object Lookup - Find any source, output, encoder, service, or canvas by name/UUID, enumerate all live instances, and duplicate sources
- Global Hotkeys - Register app/source/output hotkeys that fire system-wide via OBS's own key polling, start/stop pairs on a single key, rebindable built-in hotkeys, and key/display-string conversions
- Headless Operation - Run without GUI dependencies
- .NET 10.0 or later
- OBS Studio runtime (see OBS Runtime Setup)
OBS events, signal callbacks, and raw video/audio callbacks fire on OBS's own threads. In a callback: never block, don't touch the UI directly (marshal to your UI thread), and copy out any RawVideoFrame/RawAudioFrame data you need afterwards (the pointers are only valid during the call). Don't dispose an object from inside its own callback — it deadlocks. Calling ObsKit APIs from your own threads is fine.
Every wrapper owns a native OBS object and is IDisposable.
- Dispose in reverse order of creation;
using var obs = Obs.Initialize(...)handles shutdown. - With
Obs.AutoDisposeon (default),output.Stop()disposes the output — don't reuse it; set itfalseto start/stop an output repeatedly. takeOwnership: truelets the output dispose the encoder/service for you; otherwise that stays your responsibility.- Keep subscriptions referenced (
RawVideoSubscription,SourceAudioSubscription,SignalConnection,AudioMeter,PreviewDisplay, …) — store them in a field and dispose when done.
// Route OBS's internal log into your logger — the single most useful debugging tool.// Most failures (missing plugin, bad encoder settings, capture errors) are explained here.usingvarobs=Obs.Initialize(config =>config.WithLogging((level,message)=>Log.Information($"[OBS:{level}] {message}"))/* ... */);// Outputs and encoders report failures via return values + LastError, not exceptionsif(!recording.Start())Console.WriteLine($"Start failed: {recording.LastError}");// What actually loaded? (e.g. verify obs-browser / encoder plugins are present)foreach(varminObs.GetLoadedModules())Console.WriteLine($"{m.FileName}: {m.Name}");// Current canvas/output resolution and FPSvarinfo=Obs.GetVideoInfo();// Validate an encoder/container combination before startingif(!recording.SupportedVideoCodecs.Contains("hevc")){/* fall back to h264 */}When using DXGI Desktop Duplication for monitor capture on Windows, your application must be configured as per-monitor DPI aware.
For Windows Forms / WPF apps, add to your .csproj:
<PropertyGroup>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
</PropertyGroup>For console apps, add an app.manifest file:
<?xml version="1.0" encoding="utf-8"?>
<assemblymanifestVersion="1.0"xmlns="urn:schemas-microsoft-com:asm.v1">
<applicationxmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwarenessxmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<dpiAwarexmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
</windowsSettings>
</application>
</assembly>And reference it in your .csproj:
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>Alternatively, use MonitorCaptureMethod.WindowsGraphicsCapture which doesn't require DPI awareness.
ObsKit.NET requires OBS Studio binaries. Use the setup script to download them:
./tools/setup-obs-runtime.shThe script will prompt you for version and platform. For manual setup, download OBS from GitHub Releases.
YourApp/
├── YourApp.exe
├── obs.dll, obs-ffmpeg-mux.exe, *.dll # From OBS bin/64bit/
├── data/
│ ├── libobs/ # Shader files
│ └── obs-plugins/ # Plugin data
└── obs-plugins/64bit/ # Plugin DLLs
YourApp/
├── YourApp
├── lib/libobs.so.0 # OBS libraries
├── obs-plugins/ # Plugin .so files
└── data/libobs/, obs-plugins/ # Data files
Run with: LD_LIBRARY_PATH="$PWD/lib" ./YourApp
YourApp/
├── YourApp
└── OBS.app/Contents/
├── Frameworks/ # OBS libraries
├── PlugIns/ # Plugin .so files
└── Resources/data/ # Data files
Run with: DYLD_LIBRARY_PATH="$PWD/OBS.app/Contents/Frameworks" ./YourApp
This project wraps OBS Studio which is licensed under GPLv2. See the OBS Studio license for details.