From c67564cb46b4e927e5e1cf1b092aa51203d23ff2 Mon Sep 17 00:00:00 2001 From: shogo4405 Date: Fri, 13 Jan 2023 00:17:16 +0900 Subject: [PATCH] add ScreenCaptureKit feature. --- Examples/macOS/Base.lproj/Main.storyboard | 161 +++++++++++++++--- ...wift => CameraPublishViewController.swift} | 2 +- Examples/macOS/MenuViewController.swift | 17 +- Examples/macOS/PreferenceViewController.swift | 27 +++ .../macOS/SCStreamPublishViewController.swift | 102 +++++++++++ HaishinKit.xcodeproj/project.pbxproj | 18 +- Sources/Net/NetStream.swift | 21 +++ 7 files changed, 315 insertions(+), 33 deletions(-) rename Examples/macOS/{PublishViewController.swift => CameraPublishViewController.swift} (99%) create mode 100644 Examples/macOS/PreferenceViewController.swift create mode 100644 Examples/macOS/SCStreamPublishViewController.swift 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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