import CNDI import Foundation import MusubiCore /// Discovers NDI sources on a dedicated thread and publishes the current list. public final class NDIFinder: @unchecked Sendable { // Only touched on `thread`, except for creation and `stop()`. private let instance: NDIlib_find_instance_t private let lock = NSLock() private var running = true private var thread: Thread? public let sources: AsyncStream<[Source]> private let continuation: AsyncStream<[Source]>.Continuation public init?(showLocalSources: Bool = true) { var settings = NDIlib_find_create_t(show_local_sources: showLocalSources, p_groups: nil, p_extra_ips: nil) guard let instance = NDIlib_find_create_v2(&settings) else { return nil } self.instance = instance (sources, continuation) = AsyncStream.makeStream(bufferingPolicy: .bufferingNewest(1)) let thread = Thread { [weak self] in self?.run() } thread.name = "studio.zielke.musubi.ndi-finder" thread.qualityOfService = .utility self.thread = thread thread.start() } deinit { stop() NDIlib_find_destroy(instance) } public func stop() { lock.withLock { running = false } continuation.finish() } private var isRunning: Bool { lock.withLock { running } } private func run() { continuation.yield(currentSources()) while isRunning { // Returns early when the list changed, otherwise after the timeout. if NDIlib_find_wait_for_sources(instance, 1000) { continuation.yield(currentSources()) } } } private func currentSources() -> [Source] { var count: UInt32 = 0 guard let list = NDIlib_find_get_current_sources(instance, &count) else { return [] } return (0..