diff --git a/example.js b/example.js index 4683a1a..2b6e5f3 100644 --- a/example.js +++ b/example.js @@ -5,6 +5,7 @@ const aperture = require('.'); async function main() { const recorder = aperture(); + console.log('Screens:', await aperture.screens()); console.log('Audio devices:', await aperture.audioDevices()); console.log('Preparing to record for 5 seconds'); await recorder.startRecording(); diff --git a/index.js b/index.js index d9270c5..f48a235 100644 --- a/index.js +++ b/index.js @@ -146,12 +146,22 @@ class Aperture { module.exports = () => new Aperture(); +module.exports.screens = async () => { + const stderr = await execa.stderr(BIN, ['list-screens']); + + try { + return JSON.parse(stderr); + } catch (_) { + return stderr; + } +}; + module.exports.audioDevices = async () => { const stderr = await execa.stderr(BIN, ['list-audio-devices']); try { return JSON.parse(stderr); - } catch (err) { + } catch (_) { return stderr; } }; diff --git a/readme.md b/readme.md index e4f9887..d3ca896 100644 --- a/readme.md +++ b/readme.md @@ -43,6 +43,19 @@ See [`example.js`](example.js) if you want to quickly try it out. *(The example ## API +### aperture.screens() -> `Promise` + +Get a list of screens. The first screen is the primary screen. + +Example: + +```js +[{ + id: 69732482, + name: 'Color LCD' +}] +``` + ### aperture.audioDevices() -> `Promise` Get a list of audio devices. diff --git a/swift/aperture/DeviceList.swift b/swift/aperture/DeviceList.swift index 5047b04..e7250df 100644 --- a/swift/aperture/DeviceList.swift +++ b/swift/aperture/DeviceList.swift @@ -1,4 +1,4 @@ -import Foundation +import AppKit import AVFoundation import CoreMediaIO @@ -15,6 +15,15 @@ private func enableDalDevices() { } struct DeviceList { + static func screen() -> [[String: Any]] { + return NSScreen.screens.map { + [ + "name": $0.name, + "id": $0.id + ] + } + } + static func audio() -> [[String: String]] { return AVCaptureDevice.devices(for: .audio).map { [ diff --git a/swift/aperture/main.swift b/swift/aperture/main.swift index 50c1d55..8349608 100644 --- a/swift/aperture/main.swift +++ b/swift/aperture/main.swift @@ -65,11 +65,17 @@ func usage() { """ Usage: aperture + aperture list-screens aperture list-audio-devices """ ) } +if arguments.first == "list-screens" { + printErr(try toJson(DeviceList.screen())) + exit(0) +} + if arguments.first == "list-audio-devices" { // Uses stderr because of unrelated stuff being outputted on stdout printErr(try toJson(DeviceList.audio())) diff --git a/swift/aperture/util.swift b/swift/aperture/util.swift index 90e9808..045d8b2 100644 --- a/swift/aperture/util.swift +++ b/swift/aperture/util.swift @@ -1,4 +1,4 @@ -import Foundation +import AppKit import AVFoundation private final class StandardErrorOutputStream: TextOutputStream { @@ -24,5 +24,56 @@ extension CMTime { } extension CGDirectDisplayID { - static let main: CGDirectDisplayID = CGMainDisplayID() + static let main = CGMainDisplayID() +} + +extension NSScreen { + private func infoForCGDisplay(_ displayID: CGDirectDisplayID, options: Int) -> [AnyHashable: Any]? { + var iterator: io_iterator_t = 0 + + let result = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching("IODisplayConnect"), &iterator) + guard result == kIOReturnSuccess else { + print("Could not find services for IODisplayConnect: \(result)") + return nil + } + + var service = IOIteratorNext(iterator) + while service != 0 { + let info = IODisplayCreateInfoDictionary(service, IOOptionBits(options)).takeRetainedValue() as! [AnyHashable: Any] + + guard + let vendorID = info[kDisplayVendorID] as! UInt32?, + let productID = info[kDisplayProductID] as! UInt32? + else { + continue + } + + if vendorID == CGDisplayVendorNumber(displayID) && productID == CGDisplayModelNumber(displayID) { + return info + } + + service = IOIteratorNext(iterator) + } + + return nil + } + + var id: CGDirectDisplayID { + return deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! CGDirectDisplayID + } + + var name: String { + guard let info = infoForCGDisplay(id, options: kIODisplayOnlyPreferredName) else { + return "Unknown screen" + } + + guard + let localizedNames = info[kDisplayProductName] as? [String: Any], + let name = localizedNames.values.first as? String + else { + return "Unnamed screen" + } + + return name + } }