Last active
May 4, 2023 04:32
-
-
Save gwl/fab539cb0df2e840d57d4e6c87941eac to your computer and use it in GitHub Desktop.
Recording Manager class for SaverDebugger app
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // RecordingManager.swift | |
| // SaverDebugger | |
| // | |
| // Created by Gary W. Longsine and ChatGPT Assistant (GPT-4) on 5/2/23. | |
| // | |
| import Foundation | |
| import AVFoundation // to support recording samples of screensaver views | |
| import AppKit | |
| import ScreenSaver | |
| import os | |
| class RecordingManager { | |
| private var currentScreenSaverView: ScreenSaverView? | |
| let logger = Logger(subsystem: "com.illumineX.SaverDebugger", category: "SaverDebugger") | |
| var assetWriter: AVAssetWriter? | |
| var assetWriterInput: AVAssetWriterInput? | |
| var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor? | |
| var displayLink: CVDisplayLink? | |
| private var frameCount: Int64 = 0 | |
| // to support video recording of screensavers | |
| var outputSettings: [String: Any] = [ | |
| AVVideoCodecKey: AVVideoCodecType.h264, | |
| AVVideoWidthKey: 640, | |
| AVVideoHeightKey: 400, | |
| AVVideoCompressionPropertiesKey: [ | |
| AVVideoAverageBitRateKey: NSNumber(value: 1000000), | |
| AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel | |
| ] as [String : Any] | |
| ] | |
| var bufferAttributes: [String: Any] = [ | |
| kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32ARGB), | |
| kCVPixelBufferWidthKey as String: 640, | |
| kCVPixelBufferHeightKey as String: 400, | |
| kCVPixelBufferCGImageCompatibilityKey as String: true, | |
| kCVPixelBufferCGBitmapContextCompatibilityKey as String: true | |
| ] | |
| func startRecording(for screenSaverView: ScreenSaverView) { | |
| logger.info("SaverDebugger: startRecording() invoked...") | |
| DispatchQueue.main.async { | |
| self.logger.info("SaverDebugger: entered DispatchQueue to get screenSaverView.bounds") | |
| let screenSaverViewBounds = screenSaverView.bounds | |
| self.configureAssetWriter(with: screenSaverViewBounds) | |
| } | |
| // output handler | |
| let outputHandler: CVDisplayLinkOutputCallback = { (_, _, _, _, _, userInfo) -> CVReturn in | |
| let recordingManager = Unmanaged<RecordingManager>.fromOpaque(userInfo!).takeUnretainedValue() | |
| guard let screenSaverView = recordingManager.currentScreenSaverView else { return kCVReturnError } | |
| guard let pixelBuffer = recordingManager.createPixelBuffer(from: screenSaverView) else { return kCVReturnError } | |
| let currentTime = CMTime(value: Int64(recordingManager.frameCount), timescale: 30) | |
| if (recordingManager.assetWriterInput?.isReadyForMoreMediaData ?? false) { | |
| print("Appending pixel buffer at time \(currentTime)") | |
| recordingManager.pixelBufferAdaptor?.append(pixelBuffer, withPresentationTime: currentTime) | |
| } | |
| recordingManager.frameCount += 1 | |
| return kCVReturnSuccess | |
| } | |
| CVDisplayLinkCreateWithActiveCGDisplays(&displayLink) // Create the display link. | |
| CVDisplayLinkSetOutputCallback(displayLink!, outputHandler, Unmanaged.passUnretained(self).toOpaque()) // Set the output callback. | |
| CVDisplayLinkSetOutputHandler(self.displayLink!) { (_, _, _, _, _) -> CVReturn in | |
| self.logger.info("SaverDebugger: Output handler called") | |
| CVDisplayLinkStart(self.displayLink!) | |
| self.logger.info("SaverDebugger: CVDisplayLink started") | |
| let recordDuration : CGFloat = 7.0 // capture this many seconds of video | |
| DispatchQueue.main.asyncAfter(deadline: .now() + recordDuration) { | |
| self.logger.info("SaverDebugger: Recording for \(recordDuration) seconds...") | |
| self.stopRecording() | |
| } | |
| return kCVReturnSuccess | |
| } | |
| } | |
| func stopRecording() { | |
| DispatchQueue.main.async { [weak self] in | |
| guard let self = self else { return } | |
| if let displayLink = displayLink { | |
| CVDisplayLinkStop(displayLink) | |
| } | |
| displayLink = nil | |
| self.assetWriterInput?.markAsFinished() // Mark the asset writer input as finished. | |
| // Finalize and save the video file. | |
| self.assetWriter?.finishWriting { | |
| print("Video file successfully saved.") | |
| } | |
| } | |
| } | |
| // a helper function for the asset writer and pixel buffer | |
| func configureAssetWriter(with screenSaverViewBounds: CGRect) { | |
| let outputSettings: [String: Any] = [ | |
| AVVideoCodecKey: AVVideoCodecType.h264, | |
| AVVideoWidthKey: screenSaverViewBounds.width, | |
| AVVideoHeightKey: screenSaverViewBounds.height, | |
| AVVideoCompressionPropertiesKey: [ | |
| AVVideoAverageBitRateKey: NSNumber(value: 12_000_000), | |
| AVVideoProfileLevelKey: AVVideoProfileLevelH264High40 | |
| ] as [String: Any] // Add explicit type annotation | |
| ] | |
| print("outputSettings: \(outputSettings)") | |
| DispatchQueue.global(qos: .userInitiated).async { | |
| // assetWriter related code here | |
| do { | |
| let outputURL = self.getOutputURL() | |
| print("Output URL: \(outputURL)") | |
| self.assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: .mov) | |
| let outputSettings = [ | |
| AVVideoCodecKey: AVVideoCodecType.h264, | |
| AVVideoWidthKey: screenSaverViewBounds.width, | |
| AVVideoHeightKey: screenSaverViewBounds.height | |
| ] as [String: Any] | |
| self.assetWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings) | |
| self.assetWriterInput?.expectsMediaDataInRealTime = true | |
| let bufferAttributes = [ | |
| kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32ARGB, | |
| kCVPixelBufferWidthKey: screenSaverViewBounds.width, | |
| kCVPixelBufferHeightKey: screenSaverViewBounds.height, | |
| kCVPixelBufferCGImageCompatibilityKey: true, | |
| kCVPixelBufferCGBitmapContextCompatibilityKey: true | |
| ] as [String: Any] | |
| self.pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: self.assetWriterInput!, sourcePixelBufferAttributes: bufferAttributes) | |
| if self.assetWriter?.canAdd(self.assetWriterInput!) == true { | |
| self.assetWriter?.add(self.assetWriterInput!) | |
| } | |
| self.assetWriter!.startWriting() | |
| self.assetWriter!.startSession(atSourceTime: .zero) | |
| } catch { | |
| self.logger.info("SaverDebugger: Error starting recording: \(error.localizedDescription)") | |
| } | |
| } | |
| } | |
| // a helper function to create a CVPixelBuffer from the ScreenSaverView | |
| func createPixelBuffer(from screenSaverView: ScreenSaverView) -> CVPixelBuffer? { | |
| var pixelBuffer: CVPixelBuffer? | |
| DispatchQueue.main.sync { | |
| logger.info("SaverDebugger: entered DispatchQueue to createPixelBuffer") | |
| let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary | |
| let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(screenSaverView.bounds.width), Int(screenSaverView.bounds.height), kCVPixelFormatType_32ARGB, attrs, &pixelBuffer) | |
| if status != kCVReturnSuccess { | |
| logger.info("SaverDebugger: CVPixelBufferCreate failed with status \(status)") | |
| } | |
| CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) | |
| let pxdata = CVPixelBufferGetBaseAddress(pixelBuffer!) | |
| let rgbColorSpace = CGColorSpaceCreateDeviceRGB() | |
| let context = CGContext(data: pxdata, width: Int(screenSaverView.bounds.width), height: Int(screenSaverView.bounds.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) | |
| context?.translateBy(x: 0, y: CGFloat(screenSaverView.bounds.height)) | |
| context?.scaleBy(x: 1.0, y: -1.0) | |
| screenSaverView.layer?.render(in: context!) | |
| CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) | |
| } | |
| logger.info("SaverDebugger: returning pixelBuffer from createPixelBuffer()") | |
| return pixelBuffer | |
| } | |
| // a helper function to genrate the output URL for the video file | |
| func getOutputURL() -> URL { | |
| logger.info("SaverDebugger: entered getOutputURL()") | |
| let fileManager = FileManager.default | |
| let urls = fileManager.urls(for: .documentDirectory, in: .userDomainMask) | |
| let outputPath = urls.first?.appendingPathComponent("ScreensaverRecording.mov") | |
| return outputPath! | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment