Imported from R44VC0RP/ndi-bar (
AGENTS.md). Install upstream withnpx skills add R44VC0RP/ndi-bar. Copyright stays with the author.
AGENTS.md
Purpose
- Repository guidance for agentic coding tools working in ndi-bar.
- Project is a macOS menu bar NDI screen sender (SwiftUI + AppKit).
- Primary target:
ndi-bar; unit-test target:ndi-barTests.
Repository Layout
project.yml— xcodegen manifest. Single source of truth for the Xcode project.ndi-bar/— Swift sources, entitlements, Info.plist, Assets.xcassets.NDI/— Runtime loader and C-struct mirrors for libndi.dylib.Capture/— ScreenCaptureKit enumeration and per-display streaming.State/—StreamingController(the @MainActor ObservableObject).Update/— self-updater (GitHub Releases check, verified in-place install).Menu/— NSStatusItem/NSMenu controller.UI/— SwiftUI Settings view.
ndi-barTests/— capture configuration and lifecycle unit tests.Makefile—make gen | open | build | release | run | clean.
Tooling / Environment
- Xcode 16+ required (Swift 5.10, macOS 14+ deployment target).
xcodegenrequired to materializendi-bar.xcodeproj. Install withbrew install xcodegen; thenmake gen.libndi.dylibis loaded at runtime viadlopen. The build does NOT link against the NDI SDK, so the project compiles even without the SDK installed, but will refuse to stream until/Library/NDI SDK for Apple/exists. Do not add the NDI SDK to the Xcode project file.
Build Commands
- Generate project:
make gen - Open in Xcode:
make open - Debug build (CLI):
make build - Release build (CLI):
make release - Launch .app:
make run - Clean:
make clean
Raw xcodebuild equivalents:
xcodebuild -project ndi-bar.xcodeproj -scheme ndi-bar \
-configuration Debug -destination 'platform=macOS' build
Release / Distribution
- Default local builds are ad-hoc signed (
CODE_SIGN_IDENTITY: "-"inproject.yml).make installandmake runuse this. Good for dev, not good for handing to anyone else. - For a signed+notarized release go through
make dist. Required env:
Signing identity defaults toTEAM_ID=ABCDE12345 make distDeveloper ID Application. Notary credentials are loaded from the keychain profile inNOTARY_PROFILE(default:NDIBAR_NOTARY). Store them once viamake notary-login. make distdoes: sign Release build → verify codesign → zip → notarize viaxcrun notarytool submit --wait→ staple ticket → re-zip → sha256.- Do NOT commit a Team ID to
project.yml. Keep it in env. - Do NOT bundle
libndi.dylibinside the .app. Users install the NDI SDK themselves — required by the NDI license and keeps the app small.
Test Commands
xcodebuild -project ndi-bar.xcodeproj -scheme ndi-bar \
-destination 'platform=macOS' test
Lint / Static Checks
- No dedicated linter configured. Treat
xcodebuildwarnings as the primary static-check signal. Do not add linters unless explicitly requested.
Architecture Notes (Important)
NDIBarApp.swiftis the@mainentry point; it uses@NSApplicationDelegateAdaptorbecause menubar apps still needNSStatusItemwiring via AppKit.AppDelegateis@MainActorand owns theStreamingControllerplus theStatusMenuController.StreamingController(@MainActor,ObservableObject) coordinates:- NDI runtime loading
- display enumeration (
SCShareableContent+NSScreen.localizedName) - start/stop lifecycle per-display
- persisted preferences (
UserDefaults: sourcePrefix, fps, resolutionCap, customResolutionWidth/Height, showsCursor, captureAudio, captureMicrophone, selectedMicrophoneDeviceID, selectedCameraDeviceID, cameraMicrophoneDeviceID; migrates limitTo1080p) - screen-recording permission gate
- the camera + microphone NDI source (
CameraStreamerlifecycle, camera enumeration, camera/microphone TCC gates)
DisplayStreameris NOT @MainActor — SCStream delivery callbacks must never touch the main actor on the hot path. It uses two dedicated serial queues (captureQueue,audioQueue).CameraStreamermirrors that discipline for the camera + microphone NDI source: AVCaptureSession configuration and blocking start/stop run onsessionQueue; video/audio delivery stays onvideoQueue/audioQueue. At most one camera source runs at a time, independent of display streams.UpdateController(@MainActor, owned by AppDelegate) self-updates the app: checksreleases/latestfor repoR44VC0RP/ndi-bardaily and on demand, then downloads the release zip, verifies sha256 +codesign --verify+ Team ID match (ad-hoc dev builds accept any Developer ID team), swaps the bundle via same-volume renames, and relaunches after graceful termination. Pure logic lives inUpdateModels.swiftand is covered by unit tests. Release asset names must stayndi-bar-v<X.Y.Z>.zip(+.zip.sha256) — the updater resolves them by that convention.NDILibraryis a singleton that dlopen's libndi.dylib. All NDI C entry points are resolved viadlsymand stored as@convention(c)function pointers inNDITypes.swift. Do not introduce a bridging header.NDISendercreation and destruction run on its dedicated off-main lifecycle queue. Graceful teardown awaits explicit destruction after callback drains; RAII provides a defensive off-main fallback. One sender per display, plus one for the camera source when active.
Code Style Guidelines
Imports
- Apple frameworks first, then third-party modules (none right now).
- One import per line.
- Avoid unused imports.
Formatting
- 4-space indentation, no tabs.
- Match existing brace style in Swift files (
if,switch,Task, closures). - Prefer line breaks for long initializers over horizontal compression.
- Group related logic with
// MARK:sections in larger files.
Types and Declarations
structfor SwiftUI views, value types, and mirrors of NDI C structs.class(usuallyfinal) forObservableObject,NS*subclasses,DisplayStreamer, andNDISender.enumfor modes, state, and preference keys.- Use
private/fileprivateaggressively. - Maintain
@MainActorisolation for UI/app orchestration types. - When crossing async/background work back to UI, hop to the main actor
explicitly (
Task { @MainActor in ... }orMainActor.run { ... }).
Naming Conventions
- Types:
UpperCamelCase(DisplayStreamer,StreamingController). - Properties/functions/locals:
lowerCamelCase. - Boolean names as predicates (
isStreaming,ndiReady). - Keep NDI C struct/function names exactly as in the NDI SDK headers
(
NDIlib_video_frame_v2_t) to make grepping against docs easy.
Concurrency and Async Work
- Use
Task { ... }for async work triggered from sync callbacks. - Never block the main thread with capture I/O or NDI calls.
- SCStream delivery callbacks run on
captureQueue/audioQueue— do notTask.detachedout of them unless absolutely required; just do the NDI call inline. - NDI async sending (
NDIlib_send_send_video_async_v2) requires the pixel buffer to remain valid until the NEXT async send completes. Current code uses the synchronous v2 API to avoid that hazard. If you switch to async, implement the double-buffered retain discipline or wire up the async completion handler.
Error Handling
- Prefer graceful fallback over hard failure.
- NDI SDK missing → surface a menubar error and link to ndi.video/sdk; do not crash.
- Screen recording permission missing → show one alert on boot, then just leave the menu showing the error state.
- Use
NSLogfor diagnostic output in capture/NDI paths; avoidprint.
State Management and Side Effects
LSUIElementmust stay true. Do not introduce a Dock icon or main window.- Do not add app sandbox entitlements — screen capture and system audio capture both break under the sandbox without per-feature setup we haven't done.
- Preserve the menubar-only activation policy:
NSApp.setActivationPolicy(.accessory).
NDI trademark / licensing
- Keep the "About NDI®" menu item and Settings footer referring to NDI as a registered trademark of Vizrt NDI AB.
- Do NOT change the NDI source naming scheme to drop the attribution.
- Do not distribute
libndi.dylibinside the .app bundle; the app expects the user to install the SDK from ndi.video/sdk.
Ad-hoc TCC quirk (important for the install loop)
- Every
make installproduces a new cdhash (ad-hoc signing is content- addressable). macOS 14+ TCC binds the Screen Recording grant to cdhash, so the stored grant becomes orphaned — the Settings toggle still shows ON butCGPreflightScreenCaptureAccess()returns false. make installtherefore runstccutil reset ScreenCaptureright before launching the freshly installed .app. This is intentional; do not remove it. The reset means every install cycle leaves TCC in a "not determined" state soCGRequestScreenCaptureAccess()actually shows macOS's native prompt when the user clicks "Grant Screen Recording".make reset-tccexists as a standalone escape hatch (e.g. when debugging a stale grant from a prior bundle id / install path).- When signing with a real Developer ID (
make dist), TCC binds to Team ID instead, and the reset-on-install hack is no longer necessary.
Change Safety Checklist (for agents)
- Run
make gen && make buildafter code changes. - If you modify any file under
ndi-bar/you usually don't need to re-runmake gen; xcodegen picks up files by path. Re-run it when adding a new file AND you don't see it in Xcode, or when editingproject.yml. - If you change
UserDefaultskeys, add migration logic inStreamingController.init— users will already have values stored. ndi-bar/ndi-bar.entitlementsandndi-bar/Info.plistare GENERATED by xcodegen from theentitlements.propertiesandinfo.propertiesblocks inproject.yml. Never edit the generated files directly —make gensilently overwrites them. Both v0.3.1 camera bugs shipped this way.- Hardened-runtime (release) builds require device entitlements
(
com.apple.security.device.camera/.audio-input) or macOS silently denies access with no prompt; missingNS*UsageDescriptionkeys make TCC kill the app on first access. Ad-hoc dev builds mask both failure modes because hardened runtime is disabled. Themake signgate now verifies entitlements and usage descriptions in the built app. - If you change entitlements, re-verify that screen capture + system audio
still work in a fresh
.app(SwiftPM-built binaries have historically stripped entitlements silently; here we use xcodebuild so it should be fine, but sanity-check). - Never introduce a bridging header for NDI. If you need a new NDI symbol,
add the typealias to
NDITypes.swiftanddlsymit inNDILibrary.swift.
Commit / PR Notes (If Requested)
- Summarize user-visible behavior changes.
- Mention any new NDI symbols resolved or macOS APIs relied on.
- Note whether the build and automated tests succeed.