Skip to content

Instantly share code, notes, and snippets.

@gwl
Last active May 4, 2023 04:32
Show Gist options
  • Select an option

  • Save gwl/fab539cb0df2e840d57d4e6c87941eac to your computer and use it in GitHub Desktop.

Select an option

Save gwl/fab539cb0df2e840d57d4e6c87941eac to your computer and use it in GitHub Desktop.
Recording Manager class for SaverDebugger app
//
// 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