diff --git a/Examples/macOS/Base.lproj/Main.storyboard b/Examples/macOS/Base.lproj/Main.storyboard
index 1f4cb1f49..e4b9ef8dc 100644
--- a/Examples/macOS/Base.lproj/Main.storyboard
+++ b/Examples/macOS/Base.lproj/Main.storyboard
@@ -36,10 +36,10 @@
-
+
-
+
@@ -179,19 +179,8 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -235,13 +224,13 @@
-
+
-
+
-
+
@@ -292,7 +281,7 @@
-
+
@@ -391,5 +380,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Examples/macOS/PublishViewController.swift b/Examples/macOS/CameraPublishViewController.swift
similarity index 99%
rename from Examples/macOS/PublishViewController.swift
rename to Examples/macOS/CameraPublishViewController.swift
index 68ed1dfb1..9a0adf3d4 100644
--- a/Examples/macOS/PublishViewController.swift
+++ b/Examples/macOS/CameraPublishViewController.swift
@@ -12,7 +12,7 @@ extension NSPopUpButton {
}
}
-final class PublishViewController: NSViewController {
+final class CameraPublishViewController: NSViewController {
@IBOutlet private weak var lfView: MTHKView!
@IBOutlet private weak var audioPopUpButton: NSPopUpButton!
@IBOutlet private weak var cameraPopUpButton: NSPopUpButton!
diff --git a/Examples/macOS/MenuViewController.swift b/Examples/macOS/MenuViewController.swift
index 56c07747e..661c9316a 100644
--- a/Examples/macOS/MenuViewController.swift
+++ b/Examples/macOS/MenuViewController.swift
@@ -12,14 +12,15 @@ final class MenuViewController: NSViewController {
let factory: () -> NSViewController
}
- private let menus: [Menu] = [
- .init(title: "Publish Test", factory: { PublishViewController.getUIViewController() }),
- .init(title: "RTMP Playback Test", factory: { RTMPPlaybackViewController.getUIViewController() })
- ]
-
- override func viewDidLoad() {
- super.viewDidLoad()
- }
+ private lazy var menus: [Menu] = {
+ var menus: [Menu] = [
+ .init(title: "Publish Test", factory: { CameraPublishViewController.getUIViewController() }),
+ .init(title: "RTMP Playback Test", factory: { RTMPPlaybackViewController.getUIViewController() })
+ ]
+ menus.append(.init(title: "SCStream Publish Test", factory: { SCStreamPublishViewController.getUIViewController() }))
+ menus.append(.init(title: "Preference", factory: { PreferenceViewController.getUIViewController() }))
+ return menus
+ }()
override func viewDidAppear() {
super.viewDidAppear()
diff --git a/Examples/macOS/PreferenceViewController.swift b/Examples/macOS/PreferenceViewController.swift
new file mode 100644
index 000000000..ea4a94773
--- /dev/null
+++ b/Examples/macOS/PreferenceViewController.swift
@@ -0,0 +1,27 @@
+import AppKit
+import Foundation
+
+final class PreferenceViewController: NSViewController {
+ @IBOutlet private weak var urlField: NSTextField!
+ @IBOutlet private weak var streamNameField: NSTextField!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ urlField.stringValue = Preference.defaultInstance.uri ?? ""
+ streamNameField.stringValue = Preference.defaultInstance.streamName ?? ""
+ }
+}
+
+extension PreferenceViewController: NSTextFieldDelegate {
+ func controlTextDidChange(_ obj: Notification) {
+ guard let textFile = obj.object as? NSTextField else {
+ return
+ }
+ if textFile == urlField {
+ Preference.defaultInstance.uri = textFile.stringValue
+ }
+ if textFile == streamNameField {
+ Preference.defaultInstance.streamName = textFile.stringValue
+ }
+ }
+}
diff --git a/Examples/macOS/SCStreamPublishViewController.swift b/Examples/macOS/SCStreamPublishViewController.swift
new file mode 100644
index 000000000..33131c41f
--- /dev/null
+++ b/Examples/macOS/SCStreamPublishViewController.swift
@@ -0,0 +1,102 @@
+import AppKit
+import Foundation
+import HaishinKit
+#if canImport(ScreenCaptureKit)
+import ScreenCaptureKit
+#endif
+
+class SCStreamPublishViewController: NSViewController {
+ @IBOutlet private weak var cameraPopUpButton: NSPopUpButton!
+ @IBOutlet private weak var urlField: NSTextField!
+
+ private var currentStream: NetStream?
+ private var rtmpConnection = RTMPConnection()
+ private lazy var rtmpStream: RTMPStream = {
+ let rtmpStream = RTMPStream(connection: rtmpConnection)
+ return rtmpStream
+ }()
+
+ private var _stream: Any?
+
+ @available(macOS 12.3, *)
+ private var stream: SCStream? {
+ get {
+ _stream as? SCStream
+ }
+ set {
+ _stream = newValue
+ Task {
+ try? newValue?.addStreamOutput(rtmpStream, type: .screen, sampleHandlerQueue: DispatchQueue.main)
+ if #available(macOS 13.0, *) {
+ try? newValue?.addStreamOutput(rtmpStream, type: .audio, sampleHandlerQueue: DispatchQueue.main)
+ }
+ try? await newValue?.startCapture()
+ }
+ }
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ urlField.stringValue = Preference.defaultInstance.uri ?? ""
+ if #available(macOS 12.3, *) {
+ Task {
+ try await SCShareableContent.current.windows.forEach {
+ cameraPopUpButton.addItem(withTitle: $0.owningApplication?.applicationName ?? "")
+ }
+ }
+ }
+ }
+
+ override func viewWillAppear() {
+ super.viewWillAppear()
+ currentStream = rtmpStream
+ }
+
+ @IBAction private func selectCamera(_ sender: AnyObject) {
+ if #available(macOS 12.3, *) {
+ Task {
+ guard let window = try? await SCShareableContent.current.windows.first(where: { $0.owningApplication?.applicationName == cameraPopUpButton.title }) else {
+ return
+ }
+ let filter = SCContentFilter(desktopIndependentWindow: window)
+ let configuration = SCStreamConfiguration()
+ configuration.width = Int(window.frame.width)
+ configuration.height = Int(window.frame.height)
+ configuration.showsCursor = true
+ self.stream = SCStream(filter: filter, configuration: configuration, delegate: nil)
+ }
+ }
+ }
+
+ @IBAction private func publishOrStop(_ sender: NSButton) {
+ // Publish
+ if sender.title == "Publish" {
+ sender.title = "Stop"
+ rtmpConnection.addEventListener(.rtmpStatus, selector: #selector(rtmpStatusHandler), observer: self)
+ rtmpConnection.connect(Preference.defaultInstance.uri ?? "")
+ return
+ }
+ // Stop
+ sender.title = "Publish"
+ rtmpConnection.removeEventListener(.rtmpStatus, selector: #selector(rtmpStatusHandler), observer: self)
+ rtmpConnection.close()
+ return
+ }
+
+ @objc
+ private func rtmpStatusHandler(_ notification: Notification) {
+ let e = Event.from(notification)
+ guard
+ let data: ASObject = e.data as? ASObject,
+ let code: String = data["code"] as? String else {
+ return
+ }
+ logger.info(data)
+ switch code {
+ case RTMPConnection.Code.connectSuccess.rawValue:
+ rtmpStream.publish(Preference.defaultInstance.streamName)
+ default:
+ break
+ }
+ }
+}
diff --git a/HaishinKit.xcodeproj/project.pbxproj b/HaishinKit.xcodeproj/project.pbxproj
index 237bba4c9..9615a5b62 100644
--- a/HaishinKit.xcodeproj/project.pbxproj
+++ b/HaishinKit.xcodeproj/project.pbxproj
@@ -28,7 +28,7 @@
2915EC4D1D85BB8C00621092 /* RTMPTSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294852551D84BFAD002DE492 /* RTMPTSocket.swift */; };
2915EC541D85BDF100621092 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2915EC531D85BDF100621092 /* ReplayKit.framework */; };
291619661E7EFB09009FB344 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 291619621E7EFA2A009FB344 /* Main.storyboard */; };
- 291619691E7EFEA8009FB344 /* PublishViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291619671E7EFE4E009FB344 /* PublishViewController.swift */; };
+ 291619691E7EFEA8009FB344 /* CameraPublishViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291619671E7EFE4E009FB344 /* CameraPublishViewController.swift */; };
2916196A1E7EFF38009FB344 /* Preference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291468161E581C7D00E619BA /* Preference.swift */; };
2916196C1E7F0768009FB344 /* CMFormatDescription+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2916196B1E7F0768009FB344 /* CMFormatDescription+Extension.swift */; };
2916196D1E7F0777009FB344 /* CMFormatDescription+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2916196B1E7F0768009FB344 /* CMFormatDescription+Extension.swift */; };
@@ -419,6 +419,8 @@
BC959EEF296EE4190067BA97 /* ImageTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC959EEE296EE4190067BA97 /* ImageTransform.swift */; };
BC959EF0296EE4190067BA97 /* ImageTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC959EEE296EE4190067BA97 /* ImageTransform.swift */; };
BC959EF1296EE4190067BA97 /* ImageTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC959EEE296EE4190067BA97 /* ImageTransform.swift */; };
+ BC959F0E29705B1B0067BA97 /* SCStreamPublishViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC959F0D29705B1B0067BA97 /* SCStreamPublishViewController.swift */; };
+ BC959F1229717EDB0067BA97 /* PreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC959F1129717EDB0067BA97 /* PreferenceViewController.swift */; };
BC9CFA9323BDE8B700917EEF /* NetStreamDrawable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC9CFA9223BDE8B700917EEF /* NetStreamDrawable.swift */; };
BC9CFA9423BDE8B700917EEF /* NetStreamDrawable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC9CFA9223BDE8B700917EEF /* NetStreamDrawable.swift */; };
BC9CFA9523BDE8B700917EEF /* NetStreamDrawable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC9CFA9223BDE8B700917EEF /* NetStreamDrawable.swift */; };
@@ -724,7 +726,7 @@
2915EC521D85BDF100621092 /* Screencast.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Screencast.appex; sourceTree = BUILT_PRODUCTS_DIR; };
2915EC531D85BDF100621092 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
291619631E7EFA2A009FB344 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
- 291619671E7EFE4E009FB344 /* PublishViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublishViewController.swift; sourceTree = ""; };
+ 291619671E7EFE4E009FB344 /* CameraPublishViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPublishViewController.swift; sourceTree = ""; };
2916196B1E7F0768009FB344 /* CMFormatDescription+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CMFormatDescription+Extension.swift"; sourceTree = ""; };
2917CB652104CA2800F6823A /* AudioSpecificConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSpecificConfigTests.swift; sourceTree = ""; };
291F4E361CF206E200F59C51 /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Icon.png; sourceTree = ""; };
@@ -899,6 +901,8 @@
BC94E509263FEBB60094C169 /* MP4TrackRunBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP4TrackRunBox.swift; sourceTree = ""; };
BC94E52C264146120094C169 /* MP4ReaderConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP4ReaderConvertible.swift; sourceTree = ""; };
BC959EEE296EE4190067BA97 /* ImageTransform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTransform.swift; sourceTree = ""; };
+ BC959F0D29705B1B0067BA97 /* SCStreamPublishViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCStreamPublishViewController.swift; sourceTree = ""; };
+ BC959F1129717EDB0067BA97 /* PreferenceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceViewController.swift; sourceTree = ""; };
BC9CFA9223BDE8B700917EEF /* NetStreamDrawable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetStreamDrawable.swift; sourceTree = ""; };
BC9F9C7726F8C16600B01ED0 /* Choreographer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Choreographer.swift; sourceTree = ""; };
BCA2252B293CC5B600DD7CB2 /* IOScreenCaptureUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOScreenCaptureUnit.swift; sourceTree = ""; };
@@ -1252,9 +1256,9 @@
BCC1A72A264FAC1800661156 /* ElementaryStreamSpecificData.swift */,
BCC1A72E264FAC4E00661156 /* ElementaryStreamType.swift */,
BCC1A70A2647F23200661156 /* ESDescriptor.swift */,
+ 29B8767F1CD70AE800FC07DA /* NALUnit.swift */,
29B876801CD70AE800FC07DA /* PacketizedElementaryStream.swift */,
BCC1A726264FA1C100661156 /* ProfileLevelIndicationIndexDescriptor.swift */,
- 29B8767F1CD70AE800FC07DA /* NALUnit.swift */,
29B876811CD70AE800FC07DA /* ProgramSpecific.swift */,
BCC1A7122647F28F00661156 /* SLConfigDescriptor.swift */,
);
@@ -1279,14 +1283,16 @@
children = (
296543641D62FEB700734698 /* AppDelegate.swift */,
296543651D62FEB700734698 /* Assets.xcassets */,
+ 291619671E7EFE4E009FB344 /* CameraPublishViewController.swift */,
BC3004FA296C3FC400119932 /* Extension */,
296543671D62FEB700734698 /* Info.plist */,
291619621E7EFA2A009FB344 /* Main.storyboard */,
BC3004D3296BFFF600119932 /* MainSplitViewController.swift */,
296543691D62FEB700734698 /* MainWindowController.swift */,
BC3004F0296C0C7400119932 /* MenuViewController.swift */,
- 291619671E7EFE4E009FB344 /* PublishViewController.swift */,
+ BC959F1129717EDB0067BA97 /* PreferenceViewController.swift */,
BC3004F8296C351D00119932 /* RTMPPlaybackViewController.swift */,
+ BC959F0D29705B1B0067BA97 /* SCStreamPublishViewController.swift */,
2965436A1D62FEB700734698 /* VisualEffect.swift */,
);
path = macOS;
@@ -2485,11 +2491,13 @@
2923A1F41D6300510019FBCD /* MainWindowController.swift in Sources */,
BC3004F5296C20A300119932 /* NSObject+Extension.swift in Sources */,
BC3004D4296BFFF600119932 /* MainSplitViewController.swift in Sources */,
+ BC959F0E29705B1B0067BA97 /* SCStreamPublishViewController.swift in Sources */,
BC3004F1296C0C7400119932 /* MenuViewController.swift in Sources */,
BC3004F3296C205500119932 /* NSViewController+Extension.swift in Sources */,
+ BC959F1229717EDB0067BA97 /* PreferenceViewController.swift in Sources */,
2923A1F31D63004E0019FBCD /* VisualEffect.swift in Sources */,
2916196A1E7EFF38009FB344 /* Preference.swift in Sources */,
- 291619691E7EFEA8009FB344 /* PublishViewController.swift in Sources */,
+ 291619691E7EFEA8009FB344 /* CameraPublishViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/Sources/Net/NetStream.swift b/Sources/Net/NetStream.swift
index bdf4d4754..1532e767a 100644
--- a/Sources/Net/NetStream.swift
+++ b/Sources/Net/NetStream.swift
@@ -1,6 +1,9 @@
import AVFoundation
import CoreImage
import CoreMedia
+#if canImport(ScreenCaptureKit)
+import ScreenCaptureKit
+#endif
/// The `NetStream` class is the foundation of a RTMPStream, HTTPStream.
open class NetStream: NSObject {
@@ -296,3 +299,21 @@ extension NetStream: IOScreenCaptureUnitDelegate {
appendSampleBuffer(sampleBuffer, withType: .video)
}
}
+
+#if os(macOS)
+extension NetStream: SCStreamOutput {
+ @available(macOS 12.3, *)
+ public func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
+ if #available(macOS 13.0, *) {
+ switch type {
+ case .screen:
+ appendSampleBuffer(sampleBuffer, withType: .video)
+ default:
+ appendSampleBuffer(sampleBuffer, withType: .audio)
+ }
+ } else {
+ appendSampleBuffer(sampleBuffer, withType: .video)
+ }
+ }
+}
+#endif