Skip to content

Commit

Permalink
Merge pull request #1123 from shogo4405/feature/add-screencapturekit
Browse files Browse the repository at this point in the history
Support ScreenCaptureKit on macOS
  • Loading branch information
shogo4405 authored Jan 13, 2023
2 parents 7c8726f + c67564c commit 9c1d7a6
Show file tree
Hide file tree
Showing 7 changed files with 315 additions and 33 deletions.
161 changes: 142 additions & 19 deletions Examples/macOS/Base.lproj/Main.storyboard

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
17 changes: 9 additions & 8 deletions Examples/macOS/MenuViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
27 changes: 27 additions & 0 deletions Examples/macOS/PreferenceViewController.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
102 changes: 102 additions & 0 deletions Examples/macOS/SCStreamPublishViewController.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
18 changes: 13 additions & 5 deletions HaishinKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 = "<group>"; };
291619671E7EFE4E009FB344 /* PublishViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublishViewController.swift; sourceTree = "<group>"; };
291619671E7EFE4E009FB344 /* CameraPublishViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPublishViewController.swift; sourceTree = "<group>"; };
2916196B1E7F0768009FB344 /* CMFormatDescription+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CMFormatDescription+Extension.swift"; sourceTree = "<group>"; };
2917CB652104CA2800F6823A /* AudioSpecificConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSpecificConfigTests.swift; sourceTree = "<group>"; };
291F4E361CF206E200F59C51 /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Icon.png; sourceTree = "<group>"; };
Expand Down Expand Up @@ -899,6 +901,8 @@
BC94E509263FEBB60094C169 /* MP4TrackRunBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP4TrackRunBox.swift; sourceTree = "<group>"; };
BC94E52C264146120094C169 /* MP4ReaderConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP4ReaderConvertible.swift; sourceTree = "<group>"; };
BC959EEE296EE4190067BA97 /* ImageTransform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTransform.swift; sourceTree = "<group>"; };
BC959F0D29705B1B0067BA97 /* SCStreamPublishViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCStreamPublishViewController.swift; sourceTree = "<group>"; };
BC959F1129717EDB0067BA97 /* PreferenceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceViewController.swift; sourceTree = "<group>"; };
BC9CFA9223BDE8B700917EEF /* NetStreamDrawable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetStreamDrawable.swift; sourceTree = "<group>"; };
BC9F9C7726F8C16600B01ED0 /* Choreographer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Choreographer.swift; sourceTree = "<group>"; };
BCA2252B293CC5B600DD7CB2 /* IOScreenCaptureUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOScreenCaptureUnit.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
);
Expand All @@ -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;
Expand Down Expand Up @@ -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;
};
Expand Down
21 changes: 21 additions & 0 deletions Sources/Net/NetStream.swift
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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

0 comments on commit 9c1d7a6

Please sign in to comment.