// Recording Output APIs (macOS 15.0+)
// Stub implementation for macOS < 15.0
import Foundation
import ScreenCaptureKit
// MARK: - Recording Output (macOS 15.0+)
// Callback type definitions for recording delegate
public typealias RecordingStartedCallback = @convention(c) (UnsafeMutableRawPointer?) -> Void
public typealias RecordingFailedCallback = @convention(c) (UnsafeMutableRawPointer?, Int32, UnsafePointer<CChar>) -> Void
public typealias RecordingFinishedCallback = @convention(c) (UnsafeMutableRawPointer?) -> Void
#if SCREENCAPTUREKIT_HAS_MACOS15_SDK
// Full implementation for macOS 15 SDK
@available(macOS 15.0, *)
private class RecordingDelegate: NSObject, SCRecordingOutputDelegate {
var startedCallback: RecordingStartedCallback?
var failedCallback: RecordingFailedCallback?
var finishedCallback: RecordingFinishedCallback?
var context: UnsafeMutableRawPointer?
weak var outputRef: AnyObject?
func recordingOutputDidStartRecording(_: SCRecordingOutput) {
if let cb = startedCallback {
cb(context)
}
}
func recordingOutput(_: SCRecordingOutput, didFailWithError error: Error) {
if let cb = failedCallback {
let errorCode = extractStreamErrorCode(error)
error.localizedDescription.withCString { cb(context, errorCode, $0) }
}
}
func recordingOutputDidFinishRecording(_: SCRecordingOutput) {
if let cb = finishedCallback {
cb(context)
}
}
}
// Storage for delegate to prevent deallocation
@available(macOS 15.0, *)
private var delegateStorage: [ObjectIdentifier: RecordingDelegate] = [:]
@available(macOS 15.0, *)
private let delegateStorageLock = NSLock()
@available(macOS 15.0, *)
private func storeDelegateRef(_ delegate: RecordingDelegate, for output: SCRecordingOutput) {
delegateStorageLock.lock()
delegateStorage[ObjectIdentifier(output)] = delegate
delegateStorageLock.unlock()
}
@available(macOS 15.0, *)
private func removeDelegateRef(for output: SCRecordingOutput) {
delegateStorageLock.lock()
delegateStorage.removeValue(forKey: ObjectIdentifier(output))
delegateStorageLock.unlock()
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_configuration_create")
public func createRecordingOutputConfiguration() -> OpaquePointer {
let config = SCRecordingOutputConfiguration()
let box = Box(config)
return retain(box)
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_configuration_set_output_url")
public func setRecordingOutputURL(_ config: OpaquePointer, _ path: UnsafePointer<CChar>) {
let box: Box<SCRecordingOutputConfiguration> = unretained(config)
let pathString = String(cString: path)
box.value.outputURL = URL(fileURLWithPath: pathString)
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_configuration_set_video_codec")
public func setRecordingOutputVideoCodec(_ config: OpaquePointer, _ codec: Int32) {
let box: Box<SCRecordingOutputConfiguration> = unretained(config)
switch codec {
case 0: box.value.videoCodecType = .h264
case 1: box.value.videoCodecType = .hevc
default: break
}
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_configuration_get_video_codec")
public func getRecordingOutputVideoCodec(_ config: OpaquePointer) -> Int32 {
let box: Box<SCRecordingOutputConfiguration> = unretained(config)
switch box.value.videoCodecType {
case .h264: return 0
case .hevc: return 1
default: return 0
}
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_configuration_set_output_file_type")
public func setRecordingOutputFileType(_ config: OpaquePointer, _ fileType: Int32) {
let box: Box<SCRecordingOutputConfiguration> = unretained(config)
switch fileType {
case 0: box.value.outputFileType = .mp4
case 1: box.value.outputFileType = .mov
default: break
}
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_configuration_get_output_file_type")
public func getRecordingOutputFileType(_ config: OpaquePointer) -> Int32 {
let box: Box<SCRecordingOutputConfiguration> = unretained(config)
switch box.value.outputFileType {
case .mp4: return 0
case .mov: return 1
default: return 0
}
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_configuration_get_available_video_codecs_count")
public func getRecordingOutputAvailableVideoCodecsCount(_ config: OpaquePointer) -> Int {
let box: Box<SCRecordingOutputConfiguration> = unretained(config)
return box.value.availableVideoCodecTypes.count
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_configuration_get_available_video_codec_at")
public func getRecordingOutputAvailableVideoCodecAt(_ config: OpaquePointer, _ index: Int) -> Int32 {
let box: Box<SCRecordingOutputConfiguration> = unretained(config)
guard index >= 0, index < box.value.availableVideoCodecTypes.count else { return -1 }
let codec = box.value.availableVideoCodecTypes[index]
switch codec {
case .h264: return 0
case .hevc: return 1
default: return -1
}
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_configuration_get_available_output_file_types_count")
public func getRecordingOutputAvailableFileTypesCount(_ config: OpaquePointer) -> Int {
let box: Box<SCRecordingOutputConfiguration> = unretained(config)
return box.value.availableOutputFileTypes.count
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_configuration_get_available_output_file_type_at")
public func getRecordingOutputAvailableFileTypeAt(_ config: OpaquePointer, _ index: Int) -> Int32 {
let box: Box<SCRecordingOutputConfiguration> = unretained(config)
guard index >= 0, index < box.value.availableOutputFileTypes.count else { return -1 }
let fileType = box.value.availableOutputFileTypes[index]
switch fileType {
case .mp4: return 0
case .mov: return 1
default: return -1
}
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_configuration_retain")
public func retainRecordingOutputConfiguration(_ config: OpaquePointer) -> OpaquePointer {
let box: Box<SCRecordingOutputConfiguration> = unretained(config)
return retain(box)
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_configuration_release")
public func releaseRecordingOutputConfiguration(_ config: OpaquePointer) {
release(config)
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_create")
public func createRecordingOutput(_ config: OpaquePointer) -> OpaquePointer? {
let box: Box<SCRecordingOutputConfiguration> = unretained(config)
let delegate = RecordingDelegate()
let output = SCRecordingOutput(configuration: box.value, delegate: delegate)
// Store delegate to prevent deallocation
storeDelegateRef(delegate, for: output)
delegate.outputRef = output
return retain(output)
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_create_with_delegate")
public func createRecordingOutputWithDelegate(
_ config: OpaquePointer,
_ startedCallback: RecordingStartedCallback?,
_ failedCallback: RecordingFailedCallback?,
_ finishedCallback: RecordingFinishedCallback?,
_ context: UnsafeMutableRawPointer?
) -> OpaquePointer? {
let box: Box<SCRecordingOutputConfiguration> = unretained(config)
let delegate = RecordingDelegate()
delegate.startedCallback = startedCallback
delegate.failedCallback = failedCallback
delegate.finishedCallback = finishedCallback
delegate.context = context
let output = SCRecordingOutput(configuration: box.value, delegate: delegate)
// Store delegate to prevent deallocation
storeDelegateRef(delegate, for: output)
delegate.outputRef = output
return retain(output)
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_get_recorded_duration")
public func getRecordingOutputRecordedDuration(_ output: OpaquePointer, _ value: UnsafeMutablePointer<Int64>, _ timescale: UnsafeMutablePointer<Int32>) {
let o: SCRecordingOutput = unretained(output)
let duration = o.recordedDuration
value.pointee = duration.value
timescale.pointee = duration.timescale
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_get_recorded_file_size")
public func getRecordingOutputRecordedFileSize(_ output: OpaquePointer) -> Int64 {
let o: SCRecordingOutput = unretained(output)
return Int64(o.recordedFileSize)
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_retain")
public func retainRecordingOutput(_ output: OpaquePointer) -> OpaquePointer {
let o: SCRecordingOutput = unretained(output)
return retain(o)
}
@available(macOS 15.0, *)
@_cdecl("sc_recording_output_release")
public func releaseRecordingOutput(_ output: OpaquePointer) {
let o: SCRecordingOutput = unretained(output)
// Clean up delegate storage
removeDelegateRef(for: o)
release(output)
}
#else
// Stub implementation for older SDKs (macOS < 15 SDK)
@_cdecl("sc_recording_output_configuration_create")
public func createRecordingOutputConfiguration() -> OpaquePointer? {
nil
}
@_cdecl("sc_recording_output_configuration_set_output_url")
public func setRecordingOutputURL(_: OpaquePointer?, _: UnsafePointer<CChar>) {}
@_cdecl("sc_recording_output_configuration_set_video_codec")
public func setRecordingOutputVideoCodec(_: OpaquePointer?, _: Int32) {}
@_cdecl("sc_recording_output_configuration_get_video_codec")
public func getRecordingOutputVideoCodec(_: OpaquePointer?) -> Int32 { 0 }
@_cdecl("sc_recording_output_configuration_set_output_file_type")
public func setRecordingOutputFileType(_: OpaquePointer?, _: Int32) {}
@_cdecl("sc_recording_output_configuration_get_output_file_type")
public func getRecordingOutputFileType(_: OpaquePointer?) -> Int32 { 0 }
@_cdecl("sc_recording_output_configuration_get_available_video_codecs_count")
public func getRecordingOutputAvailableVideoCodecsCount(_: OpaquePointer?) -> Int { 0 }
@_cdecl("sc_recording_output_configuration_get_available_video_codec_at")
public func getRecordingOutputAvailableVideoCodecAt(_: OpaquePointer?, _: Int) -> Int32 { -1 }
@_cdecl("sc_recording_output_configuration_get_available_output_file_types_count")
public func getRecordingOutputAvailableFileTypesCount(_: OpaquePointer?) -> Int { 0 }
@_cdecl("sc_recording_output_configuration_get_available_output_file_type_at")
public func getRecordingOutputAvailableFileTypeAt(_: OpaquePointer?, _: Int) -> Int32 { -1 }
@_cdecl("sc_recording_output_configuration_retain")
public func retainRecordingOutputConfiguration(_: OpaquePointer?) -> OpaquePointer? {
nil
}
@_cdecl("sc_recording_output_configuration_release")
public func releaseRecordingOutputConfiguration(_: OpaquePointer?) {}
@_cdecl("sc_recording_output_create")
public func createRecordingOutput(_: OpaquePointer?) -> OpaquePointer? {
nil
}
@_cdecl("sc_recording_output_create_with_delegate")
public func createRecordingOutputWithDelegate(
_: OpaquePointer?,
_: RecordingStartedCallback?,
_: RecordingFailedCallback?,
_: RecordingFinishedCallback?,
_: UnsafeMutableRawPointer?
) -> OpaquePointer? {
nil
}
@_cdecl("sc_recording_output_get_recorded_duration")
public func getRecordingOutputRecordedDuration(_: OpaquePointer?, _ value: UnsafeMutablePointer<Int64>, _ timescale: UnsafeMutablePointer<Int32>) {
value.pointee = 0
timescale.pointee = 0
}
@_cdecl("sc_recording_output_get_recorded_file_size")
public func getRecordingOutputRecordedFileSize(_: OpaquePointer?) -> Int64 { 0 }
@_cdecl("sc_recording_output_retain")
public func retainRecordingOutput(_: OpaquePointer?) -> OpaquePointer? {
nil
}
@_cdecl("sc_recording_output_release")
public func releaseRecordingOutput(_: OpaquePointer?) {}
#endif