mirror of
https://github.com/neon443/Scream.git
synced 2026-03-11 13:26:18 +00:00
more stuff for the recorder
apple sample code my goat
This commit is contained in:
@@ -22,10 +22,47 @@ struct CapturedFrame {
|
|||||||
|
|
||||||
class CaptureEngine: NSObject {
|
class CaptureEngine: NSObject {
|
||||||
private var stream: SCStream?
|
private var stream: SCStream?
|
||||||
var streamOutput: StreamDelegate?
|
var streamOutput: StreamHandler?
|
||||||
|
let videoSampleBufferQueue = DispatchQueue(label: "videoSampleBufferQueue")
|
||||||
|
let audioSampleBufferQueue = DispatchQueue(label: "audioSampleBufferQueue")
|
||||||
|
let micSampleBufferQueue = DispatchQueue(label: "micSampleBufferQueue")
|
||||||
|
|
||||||
|
private var continuation: AsyncThrowingStream<CapturedFrame, Error>.Continuation?
|
||||||
|
|
||||||
func startCapture(config: SCStreamConfiguration, filter: SCContentFilter) -> AsyncThrowingStream<CapturedFrame, Error> {
|
func startCapture(config: SCStreamConfiguration, filter: SCContentFilter) -> AsyncThrowingStream<CapturedFrame, Error> {
|
||||||
AsyncThrowingStream<CapturedFrame, Error> { continuation in
|
AsyncThrowingStream<CapturedFrame, Error> { continuation in
|
||||||
// let streamOutput = SCStreamOutput
|
let streamOutput = StreamHandler(continuation: continuation)
|
||||||
|
self.streamOutput = streamOutput
|
||||||
|
streamOutput.frameBufferHandler = { continuation.yield($0) }
|
||||||
|
streamOutput.pcmBufferHandler = { print($0) }
|
||||||
|
|
||||||
|
do {
|
||||||
|
stream = SCStream(filter: filter, configuration: config, delegate: streamOutput)
|
||||||
|
|
||||||
|
try stream?.addStreamOutput(streamOutput, type: .screen, sampleHandlerQueue: videoSampleBufferQueue)
|
||||||
|
try stream?.addStreamOutput(streamOutput, type: .audio, sampleHandlerQueue: audioSampleBufferQueue)
|
||||||
|
try stream?.addStreamOutput(streamOutput, type: .microphone, sampleHandlerQueue: videoSampleBufferQueue)
|
||||||
|
} catch {
|
||||||
|
continuation.finish(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopCapture() async {
|
||||||
|
do {
|
||||||
|
try await stream?.stopCapture()
|
||||||
|
continuation?.finish()
|
||||||
|
} catch {
|
||||||
|
continuation?.finish(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(config: SCStreamConfiguration, filter: SCContentFilter) async {
|
||||||
|
do {
|
||||||
|
try await stream?.updateConfiguration(config)
|
||||||
|
try await stream?.updateContentFilter(filter)
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,16 +82,16 @@ class StreamHandler: NSObject, SCStreamOutput, SCStreamDelegate {
|
|||||||
|
|
||||||
switch type {
|
switch type {
|
||||||
case .screen:
|
case .screen:
|
||||||
// guard let frame =
|
guard let frame = createFrame(for: sampleBuffer) else { return }
|
||||||
|
frameBufferHandler?(frame)
|
||||||
case .audio:
|
case .audio:
|
||||||
<#code#>
|
handleAudio(for: sampleBuffer)
|
||||||
case .microphone:
|
case .microphone:
|
||||||
<#code#>
|
print("idk what to do with mic buffers")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFrame(for sampleBuffer: CMSampleBuffer) -> CapturedFrame? {
|
func createFrame(for sampleBuffer: CMSampleBuffer) -> CapturedFrame? {
|
||||||
|
|
||||||
guard let attachmentsArr = CMSampleBufferGetSampleAttachmentsArray(
|
guard let attachmentsArr = CMSampleBufferGetSampleAttachmentsArray(
|
||||||
sampleBuffer,
|
sampleBuffer,
|
||||||
createIfNecessary: false
|
createIfNecessary: false
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
A98E8BF92F05B2A5006D4458 /* ScreamUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98E8BF62F05B2A5006D4458 /* ScreamUITests.swift */; };
|
A98E8BF92F05B2A5006D4458 /* ScreamUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98E8BF62F05B2A5006D4458 /* ScreamUITests.swift */; };
|
||||||
A98E8BFA2F05B2A5006D4458 /* ScreamUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98E8BF72F05B2A5006D4458 /* ScreamUITestsLaunchTests.swift */; };
|
A98E8BFA2F05B2A5006D4458 /* ScreamUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98E8BF72F05B2A5006D4458 /* ScreamUITestsLaunchTests.swift */; };
|
||||||
A98E8BFD2F05D28D006D4458 /* ScreenRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98E8BFC2F05D28D006D4458 /* ScreenRecorder.swift */; };
|
A98E8BFD2F05D28D006D4458 /* ScreenRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98E8BFC2F05D28D006D4458 /* ScreenRecorder.swift */; };
|
||||||
A98E8C012F06F496006D4458 /* StreamDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98E8C002F06F496006D4458 /* StreamDelegate.swift */; };
|
|
||||||
A9D7225F2F070FE600050BB0 /* CaptureEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D7225E2F070FE600050BB0 /* CaptureEngine.swift */; };
|
A9D7225F2F070FE600050BB0 /* CaptureEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D7225E2F070FE600050BB0 /* CaptureEngine.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@@ -47,7 +46,6 @@
|
|||||||
A98E8BF72F05B2A5006D4458 /* ScreamUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreamUITestsLaunchTests.swift; sourceTree = "<group>"; };
|
A98E8BF72F05B2A5006D4458 /* ScreamUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreamUITestsLaunchTests.swift; sourceTree = "<group>"; };
|
||||||
A98E8BFB2F05C7B6006D4458 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
A98E8BFB2F05C7B6006D4458 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
A98E8BFC2F05D28D006D4458 /* ScreenRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenRecorder.swift; sourceTree = "<group>"; };
|
A98E8BFC2F05D28D006D4458 /* ScreenRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenRecorder.swift; sourceTree = "<group>"; };
|
||||||
A98E8C002F06F496006D4458 /* StreamDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamDelegate.swift; sourceTree = "<group>"; };
|
|
||||||
A9D7225E2F070FE600050BB0 /* CaptureEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureEngine.swift; sourceTree = "<group>"; };
|
A9D7225E2F070FE600050BB0 /* CaptureEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureEngine.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@@ -105,7 +103,6 @@
|
|||||||
A98E8BEC2F05B2A0006D4458 /* Assets.xcassets */,
|
A98E8BEC2F05B2A0006D4458 /* Assets.xcassets */,
|
||||||
A98E8BEE2F05B2A0006D4458 /* MainMenu.xib */,
|
A98E8BEE2F05B2A0006D4458 /* MainMenu.xib */,
|
||||||
A98E8BFC2F05D28D006D4458 /* ScreenRecorder.swift */,
|
A98E8BFC2F05D28D006D4458 /* ScreenRecorder.swift */,
|
||||||
A98E8C002F06F496006D4458 /* StreamDelegate.swift */,
|
|
||||||
);
|
);
|
||||||
path = Scream;
|
path = Scream;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -266,7 +263,6 @@
|
|||||||
files = (
|
files = (
|
||||||
A98E8BF02F05B2A0006D4458 /* AppDelegate.swift in Sources */,
|
A98E8BF02F05B2A0006D4458 /* AppDelegate.swift in Sources */,
|
||||||
A9D7225F2F070FE600050BB0 /* CaptureEngine.swift in Sources */,
|
A9D7225F2F070FE600050BB0 /* CaptureEngine.swift in Sources */,
|
||||||
A98E8C012F06F496006D4458 /* StreamDelegate.swift in Sources */,
|
|
||||||
A98E8BFD2F05D28D006D4458 /* ScreenRecorder.swift in Sources */,
|
A98E8BFD2F05D28D006D4458 /* ScreenRecorder.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|||||||
@@ -11,15 +11,37 @@ import ScreenCaptureKit
|
|||||||
class ScreenRecorder: NSObject {
|
class ScreenRecorder: NSObject {
|
||||||
var isRunning: Bool = false
|
var isRunning: Bool = false
|
||||||
var isAppExluded: Bool = false
|
var isAppExluded: Bool = false
|
||||||
var isAudioEnabled: Bool = true
|
var isAudioEnabled: Bool = false
|
||||||
var filter: SCContentFilter?
|
|
||||||
var streamConfig = SCStreamConfiguration()
|
|
||||||
var stream: SCStream?
|
|
||||||
var streamDelegate = StreamDelegate()
|
|
||||||
var streamOutput = StreamOutputDelegate()
|
|
||||||
|
|
||||||
let videoSampleBufferQueue = DispatchQueue(label: "videoSampleBufferQueue")
|
var filter: SCContentFilter?
|
||||||
let audioSampleBufferQueue = DispatchQueue(label: "audioSampleBufferQueue")
|
// var filter: SCContentFilter
|
||||||
|
//
|
||||||
|
// var excludedApps = [SCRunningApplication]()
|
||||||
|
// //if users exclude Scream from the screen share
|
||||||
|
// //exclude by matching bundleid
|
||||||
|
//// if isAppExluded {
|
||||||
|
//// excludedApps = availableContent.applications.filter { app in
|
||||||
|
//// Bundle.main.bundleIdentifier == app.bundleIdentifier
|
||||||
|
//// }
|
||||||
|
//// }
|
||||||
|
// filter = SCContentFilter(display: availableContent.displays.first!, excludingApplications: excludedApps, exceptingWindows: [])
|
||||||
|
// }
|
||||||
|
|
||||||
|
var streamConfig: SCStreamConfiguration {
|
||||||
|
var streamConfig = SCStreamConfiguration()
|
||||||
|
//TODO: hdr
|
||||||
|
streamConfig.capturesAudio = isAudioEnabled
|
||||||
|
streamConfig.excludesCurrentProcessAudio = false
|
||||||
|
// streamConfig.captureMicrophone = true
|
||||||
|
|
||||||
|
streamConfig.width = Int(NSScreen.main?.frame.width ?? 100)
|
||||||
|
streamConfig.height = Int(NSScreen.main?.frame.height ?? 100)
|
||||||
|
|
||||||
|
streamConfig.minimumFrameInterval = CMTime(value: 1, timescale: 20)
|
||||||
|
streamConfig.queueDepth = 5
|
||||||
|
return streamConfig
|
||||||
|
}
|
||||||
|
let captureEngine = CaptureEngine()
|
||||||
|
|
||||||
var canRecord: Bool {
|
var canRecord: Bool {
|
||||||
true
|
true
|
||||||
@@ -35,7 +57,6 @@ class ScreenRecorder: NSObject {
|
|||||||
print(error.localizedDescription)
|
print(error.localizedDescription)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var excludedApps = [SCRunningApplication]()
|
var excludedApps = [SCRunningApplication]()
|
||||||
//if users exclude Scream from the screen share
|
//if users exclude Scream from the screen share
|
||||||
//exclude by matching bundleid
|
//exclude by matching bundleid
|
||||||
@@ -46,25 +67,22 @@ class ScreenRecorder: NSObject {
|
|||||||
}
|
}
|
||||||
filter = SCContentFilter(display: availableContent.displays.first!, excludingApplications: excludedApps, exceptingWindows: [])
|
filter = SCContentFilter(display: availableContent.displays.first!, excludingApplications: excludedApps, exceptingWindows: [])
|
||||||
|
|
||||||
//TODO: hdr
|
do {
|
||||||
|
isRunning = true
|
||||||
|
for try await frame in captureEngine.startCapture(config: streamConfig, filter: filter!) {
|
||||||
|
print(frame)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
isRunning = false
|
||||||
|
print(error.localizedDescription)
|
||||||
|
}
|
||||||
|
//TODO: update the config using stream.updateConfiguration or .updateContentFilter
|
||||||
|
}
|
||||||
|
|
||||||
streamConfig.capturesAudio = isAudioEnabled
|
func stop() async {
|
||||||
streamConfig.excludesCurrentProcessAudio = true
|
guard isRunning else { return }
|
||||||
// streamConfig.captureMicrophone = true
|
await captureEngine.stopCapture()
|
||||||
|
isRunning = false
|
||||||
streamConfig.width = Int(NSScreen.main?.frame.width ?? 100)
|
|
||||||
streamConfig.height = Int(NSScreen.main?.frame.height ?? 100)
|
|
||||||
|
|
||||||
streamConfig.minimumFrameInterval = CMTime(value: 1, timescale: 20)
|
|
||||||
streamConfig.queueDepth = 5
|
|
||||||
|
|
||||||
stream = SCStream(filter: filter!, configuration: streamConfig, delegate: streamDelegate)
|
|
||||||
|
|
||||||
try! stream?.addStreamOutput(streamOutput, type: .screen, sampleHandlerQueue: videoSampleBufferQueue)
|
|
||||||
try! stream?.addStreamOutput(streamOutput, type: .audio, sampleHandlerQueue: audioSampleBufferQueue)
|
|
||||||
// try! stream?.addStreamOutput(streamOutput, type: .microphone, sampleHandlerQueue: videoSampleBufferQueue)
|
|
||||||
|
|
||||||
//update the config using stream.updateConfiguration or .updateContentFilter
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
//
|
|
||||||
// StreamDelegate.swift
|
|
||||||
// Scream
|
|
||||||
//
|
|
||||||
// Created by neon443 on 01/01/2026.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import ScreenCaptureKit
|
|
||||||
|
|
||||||
class StreamDelegate: NSObject, SCStreamDelegate {
|
|
||||||
func outputVideoEffectDidStart(for stream: SCStream) {
|
|
||||||
print("presenter overlay started")
|
|
||||||
}
|
|
||||||
|
|
||||||
func outputVideoEffectDidStop(for stream: SCStream) {
|
|
||||||
print("presenter overlay stopped")
|
|
||||||
}
|
|
||||||
|
|
||||||
func streamDidBecomeActive(_ stream: SCStream) {
|
|
||||||
print("stream became Active")
|
|
||||||
}
|
|
||||||
|
|
||||||
func streamDidBecomeInactive(_ stream: SCStream) {
|
|
||||||
print("stream became Inactive")
|
|
||||||
}
|
|
||||||
|
|
||||||
func stream(_ stream: SCStream, didStopWithError error: any Error) {
|
|
||||||
print(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user