Article / 2026/05/29
录制
录制 的技术笔记。
//
// YSVideoRecoder.swift
// wscanner
//
// Created by yunshen on 2025/9/5.
//
import AVFoundation
import MetalKit
class YSVideoRecoder: IRecoder {
func startRecording(outputURL: URL, playbackSpeed: Double, maxFrames: Int64, autoStopCompletion: ((Bool, URL?) -> Void)?) -> Bool {
return true
}
private weak var delegate: VideoRecorderDelegate?
private var isRecording: Bool = false
private var videoWidth: Int = 0
private var videoHeight: Int = 0
private let recordingQueue = DispatchQueue(
label: "com.videorecorder.queue",
qos: .userInitiated
)
private var pixelBufferPool: CVPixelBufferPool?
private var reusablePixelData: UnsafeMutablePointer<UInt8>?
private var pixelDataSize: Int = 0
private var videoSpeedMultiplier: Double = 1.0
private var assetWriter: AVAssetWriter?
private var assetWriterInput: AVAssetWriterInput?
private var assetWriterPixelBufferAdaptor:
AVAssetWriterInputPixelBufferAdaptor?
// 时间管理
private var recordingStartTime: CMTime?
private var recordingStartTimeAbs: CFAbsoluteTime = 0
private var lastCaptureTime: CFAbsoluteTime = 0
private var lastPresentationTime: CMTime = .zero
private var firstValidFrameCaptured: Bool = false
var frameCount: Int64 = 0
// 帧缓冲
private var pendingFrames = [PendingFrame]()
private let pendingFramesLock = NSLock()
private var isProcessingFrames = false
private var device: MTLDevice
private var captureBuffer: MTLBuffer?
private var captureCommandQueue: MTLCommandQueue?
init(device: MTLDevice) {
self.device = device
self.captureCommandQueue = device.makeCommandQueue()
}
deinit {
cleanupResources()
}
func setDelegate(delegate: any VideoRecorderDelegate) {
self.delegate = delegate
}
func configureSize(width: Int, height: Int) {
guard !isRecording else {
print("无法在录制中更改尺寸")
return
}
videoWidth = width
videoHeight = height
// 更新像素数据缓冲区
pixelDataSize = width * height * 4
reusablePixelData?.deallocate()
reusablePixelData = UnsafeMutablePointer<UInt8>.allocate(
capacity: pixelDataSize
)
self.captureBuffer = device.makeBuffer(
length: pixelDataSize,
options: .storageModeShared
)
}
/// 开始录制
func startRecording(outputURL: URL, playbackSpeed: Double = 1.0) -> Bool {
guard !isRecording else {
print("已经在录制中")
return false
}
guard videoWidth > 0 && videoHeight > 0 else {
print("请先配置视频尺寸")
return false
}
videoSpeedMultiplier = playbackSpeed
do {
// 移除已存在的文件
if FileManager.default.fileExists(atPath: outputURL.path) {
try FileManager.default.removeItem(at: outputURL)
}
// 创建 AVAssetWriter
assetWriter = try AVAssetWriter(
outputURL: outputURL,
fileType: .mp4
)
// 配置视频设置 - 修复1: 使用正确的视频编码设置
let videoSettings = createRealVideoSettings()
assetWriterInput = AVAssetWriterInput(
mediaType: .video,
outputSettings: videoSettings
)
assetWriterInput?.expectsMediaDataInRealTime = true // 修复2: 改为 true
// 创建像素缓冲适配器
let pixelBufferAttributes = createPixelBufferAttributes()
assetWriterPixelBufferAdaptor =
AVAssetWriterInputPixelBufferAdaptor(
assetWriterInput: assetWriterInput!,
sourcePixelBufferAttributes: pixelBufferAttributes
)
// 添加输入到 writer
if assetWriter!.canAdd(assetWriterInput!) {
assetWriter!.add(assetWriterInput!)
} else {
print("无法添加视频输入到 AssetWriter")
return false
}
// 开始写入
if !assetWriter!.startWriting() {
print(
"无法开始写入,错误: \(assetWriter!.error?.localizedDescription ?? "未知错误")"
)
return false
}
// 开始会话
assetWriter!.startSession(atSourceTime: .zero)
// 更新状态
isRecording = true
firstValidFrameCaptured = false
frameCount = 0
lastCaptureTime = 0
// 启动帧处理
startFrameProcessing()
// 通知代理
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.delegate?.videoRecorderDidStartRecording(self)
}
print("录制开始成功")
return true
} catch {
print("开始录制失败: \(error)")
return false
}
}
func stopRecording(completion: @escaping (Bool, URL?) -> Void) {
guard isRecording, let assetWriter = self.assetWriter else {
completion(false, nil)
return
}
isRecording = false
firstValidFrameCaptured = false
isProcessingFrames = false
print("正在停止录制,处理剩余帧...")
// 在后台队列处理剩余帧并完成写入
recordingQueue.async { [weak self] in
guard let self = self else { return }
// 处理剩余的帧
self.processPendingFrames()
// 等待输入准备好
while !(self.assetWriterInput?.isReadyForMoreMediaData ?? false) {
Thread.sleep(forTimeInterval: 0.01)
}
// 标记输入完成
self.assetWriterInput?.markAsFinished()
// 完成写入
assetWriter.finishWriting {
DispatchQueue.main.async {
let success = assetWriter.status == .completed
let outputURL = success ? assetWriter.outputURL : nil
if !success {
print("录制失败,状态: \(assetWriter.status.rawValue)")
if let error = assetWriter.error {
print("错误信息: \(error)")
}
} else {
print("录制成功完成,共 \(self.frameCount) 帧")
}
// 清理资源
self.cleanupRecordingResources()
// 通知代理
self.delegate?.videoRecorderDidStopRecording(
self,
success: success,
outputURL: outputURL
)
// 调用完成回调
completion(success, outputURL)
}
}
}
}
func captureFrame(from texture: MTLTexture) {
guard isRecording else { return }
let currentTime = CFAbsoluteTimeGetCurrent()
lastCaptureTime = currentTime
let presentationTime: CMTime
if !firstValidFrameCaptured {
presentationTime = .zero
// 初始化时间
recordingStartTimeAbs = CFAbsoluteTimeGetCurrent()
lastPresentationTime = .zero
recordingStartTime = .zero
firstValidFrameCaptured = true
} else {
presentationTime = calculatePresentationTimeReal()
}
guard let commandQueue = captureCommandQueue,
let buffer = captureBuffer
else {
print("Metal resources not initialized")
return
}
guard let commandBuffer = commandQueue.makeCommandBuffer(),
let blitEncoder = commandBuffer.makeBlitCommandEncoder()
else {
return
}
// 异步复制纹理到缓冲区
blitEncoder.copy(
from: texture,
sourceSlice: 0,
sourceLevel: 0,
sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0),
sourceSize: MTLSize(
width: texture.width,
height: texture.height,
depth: 1
),
to: buffer,
destinationOffset: 0,
destinationBytesPerRow: texture.width * 4,
destinationBytesPerImage: texture.width * texture.height * 4
)
blitEncoder.endEncoding()
// 添加完成回调,在 GPU 复制完成后处理数据
commandBuffer.addCompletedHandler { [weak self] _ in
guard let self = self else { return }
// 现在数据已经在 buffer 中,创建 Data 对象
let bufferPointer = buffer.contents()
let frameData = Data(bytes: bufferPointer,
count: pixelDataSize)
let pendingFrame = PendingFrame(
pixelData: frameData,
presentationTime: presentationTime,
width: self.videoWidth,
height: self.videoHeight
)
// 添加到待处理队列
self.pendingFramesLock.lock()
self.pendingFrames.append(pendingFrame)
self.pendingFramesLock.unlock()
}
commandBuffer.commit()
}
func isDecodering() -> Bool {
return isRecording
}
private func startFrameProcessing() {
isProcessingFrames = true
recordingQueue.async { [weak self] in
while self?.isProcessingFrames == true {
self?.processPendingFrames()
Thread.sleep(forTimeInterval: 0.01) // 更频繁地处理
}
}
}
private func processPendingFrames() {
pendingFramesLock.lock()
let framesToProcess = pendingFrames
pendingFrames.removeAll()
pendingFramesLock.unlock()
for frame in framesToProcess {
processFrame(frame)
}
}
private func processFrame(_ frame: PendingFrame) {
guard let assetWriterInput = self.assetWriterInput,
let pixelBufferAdaptor = self.assetWriterPixelBufferAdaptor
else {
return
}
// 等待输入准备好
while !assetWriterInput.isReadyForMoreMediaData && isProcessingFrames {
Thread.sleep(forTimeInterval: 0.01)
}
guard assetWriterInput.isReadyForMoreMediaData else { return }
autoreleasepool {
// 创建像素缓冲区 - 修复4: 确保从池中获取
var pixelBuffer: CVPixelBuffer?
let pool = pixelBufferAdaptor.pixelBufferPool
if let pool = pool {
let status = CVPixelBufferPoolCreatePixelBuffer(
kCFAllocatorDefault,
pool,
&pixelBuffer
)
if status != kCVReturnSuccess {
print("从池创建像素缓冲区失败: \(status)")
}
}
// 如果池中没有可用的,创建新的
if pixelBuffer == nil {
let attributes = createPixelBufferAttributes()
let status = CVPixelBufferCreate(
kCFAllocatorDefault,
frame.width,
frame.height,
kCVPixelFormatType_32BGRA,
attributes as CFDictionary,
&pixelBuffer
)
if status != kCVReturnSuccess {
print("创建像素缓冲区失败: \(status)")
return
}
}
guard let buffer = pixelBuffer else {
print("无法创建像素缓冲区")
return
}
// 转换并复制像素数据
CVPixelBufferLockBaseAddress(
buffer,
CVPixelBufferLockFlags(rawValue: 0)
)
if let baseAddress = CVPixelBufferGetBaseAddress(buffer) {
let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
let dstBuffer = baseAddress.assumingMemoryBound(to: UInt8.self)
frame.pixelData.withUnsafeBytes { srcBytes in
if let srcBuffer = srcBytes.bindMemory(to: UInt8.self)
.baseAddress
{
copyBGRA(
src: srcBuffer,
dst: dstBuffer,
width: frame.width,
height: frame.height,
dstBytesPerRow: bytesPerRow
)
}
}
}
CVPixelBufferUnlockBaseAddress(
buffer,
CVPixelBufferLockFlags(rawValue: 0)
)
// 添加到视频 - 修复5: 确保时间戳正确
if pixelBufferAdaptor.append(
buffer,
withPresentationTime: frame.presentationTime
) {
frameCount += 1
lastPresentationTime = frame.presentationTime
// 通知代理(在主线程)
if frameCount % 30 == 0 { // 每30帧通知一次,减少开销
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.delegate?.videoRecorderDidCaptureFrame(
self,
frameCount: self.frameCount
)
}
}
} else {
print("添加帧失败,时间戳: \(frame.presentationTime.seconds)")
}
}
}
private func calculatePresentationTimeReal() -> CMTime {
let currentTimeAbs = CFAbsoluteTimeGetCurrent()
let elapsedTime = currentTimeAbs - recordingStartTimeAbs
let adjustedTime = elapsedTime * videoSpeedMultiplier
return CMTime(seconds: adjustedTime, preferredTimescale: 600)
}
private func cleanupRecordingResources() {
isProcessingFrames = false
assetWriter = nil
assetWriterInput = nil
assetWriterPixelBufferAdaptor = nil
recordingStartTime = nil
recordingStartTimeAbs = 0
frameCount = 0
pixelBufferPool = nil
lastPresentationTime = .zero
pendingFramesLock.lock()
pendingFrames.removeAll()
pendingFramesLock.unlock()
}
private func cleanupResources() {
cleanupRecordingResources()
reusablePixelData?.deallocate()
reusablePixelData = nil
}
private func copyBGRA(
src: UnsafePointer<UInt8>,
dst: UnsafeMutablePointer<UInt8>,
width: Int,
height: Int,
dstBytesPerRow: Int
) {
// memcpy(dst, src, width * height * 4)
let srcBytesPerRow = width * 4
// print("dstBytesPerRow = \(dstBytesPerRow) srcBytesPerRow = \(srcBytesPerRow)")
for y in 0..<height {
let srcRowStart = y * srcBytesPerRow
let dstRowStart = y * dstBytesPerRow
memcpy(dst + dstRowStart, src + srcRowStart, srcBytesPerRow)
}
}
private func createRealVideoSettings() -> [String: Any] {
return [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: videoWidth,
AVVideoHeightKey: videoHeight,
AVVideoCompressionPropertiesKey: [
AVVideoAverageBitRateKey: NSNumber(
value: videoWidth * videoHeight * 10
), // 提高比特率
AVVideoProfileLevelKey: AVVideoProfileLevelH264High41, // 使用更高的配置
// AVVideoProfileLevelKey:
// AVVideoProfileLevelH264BaselineAutoLevel,
AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC,
],
]
}
private func createPixelBufferAttributes() -> [String: Any] {
return [
kCVPixelBufferPixelFormatTypeKey as String:
kCVPixelFormatType_32BGRA,
kCVPixelBufferWidthKey as String: videoWidth,
kCVPixelBufferHeightKey as String: videoHeight,
kCVPixelBufferOpenGLESCompatibilityKey as String: true,
kCVPixelBufferIOSurfacePropertiesKey as String: [:],
kCVPixelBufferCGImageCompatibilityKey as String: true,
kCVPixelBufferCGBitmapContextCompatibilityKey as String: true,
]
}
}
//
// YSVideoRecoder2.swift
// wscanner
//
// Created by yunshen on 2025/9/5.
//
internal import common
import AVFoundation
import MetalKit
import Foundation
import AVFoundation
import MetalKit
// MARK: - 改进的帧数据容器
class ReusableFrameData {
var pixelData: UnsafeMutablePointer<UInt8>
let capacity: Int
var presentationTime: CMTime = .zero
var width: Int = 0
var height: Int = 0
var isValid: Bool = false
init(capacity: Int) {
self.capacity = capacity
self.pixelData = UnsafeMutablePointer<UInt8>.allocate(capacity: capacity)
print("ReusableFrameData init - capacity: \(capacity)")
}
deinit {
pixelData.deallocate()
print("ReusableFrameData deinit - 不应该频繁出现此消息")
}
func reset() {
isValid = false
presentationTime = .zero
width = 0
height = 0
}
func copyFrom(buffer: UnsafeRawPointer, size: Int, time: CMTime, width: Int, height: Int) {
guard size <= capacity else {
print("Error: size \(size) > capacity \(capacity)")
return
}
memcpy(pixelData, buffer, size)
self.presentationTime = time
self.width = width
self.height = height
self.isValid = true
}
}
// MARK: - 优化后的视频录制器
class YSVideoRecoder2: IRecoder {
private weak var delegate: VideoRecorderDelegate?
private var isRecording: Bool = false
private var videoWidth: Int = 0
private var videoHeight: Int = 0
private let recordingQueue = DispatchQueue(
label: "com.videorecorder.queue",
qos: .userInitiated
)
// 使用强引用池
private var frameDataPool: StrongSynchronizedPool<ReusableFrameData>!
private var metalBufferPool: StrongSynchronizedPool<MTLBuffer>!
private var pixelDataSize: Int = 0
private var videoSpeedMultiplier: Double = 1.0
private var assetWriter: AVAssetWriter?
private var assetWriterInput: AVAssetWriterInput?
private var assetWriterPixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor?
// 时间管理
private var recordingStartTime: CMTime?
private var recordingStartTimeAbs: CFAbsoluteTime = 0
private var lastCaptureTime: CFAbsoluteTime = 0
private var lastPresentationTime: CMTime = .zero
private var firstValidFrameCaptured: Bool = false
var frameCount: Int64 = 0
// 直接使用 ReusableFrameData 的队列
private var isProcessingFrames = false
private var linkBlockingQueue = LinkedBlockingQueue<ReusableFrameData>(
capacity: 200,
target: "YSVideoRecoder2",
isPrint: false
)
private var thread: YSThread<ReusableFrameData>? = nil
private var device: MTLDevice
private var captureCommandQueue: MTLCommandQueue?
// 状态管理
private let stateLock = NSLock()
private var isFinishing = false
private var pendingFrameCount = 0
private let pendingFrameLock = NSLock()
// 帧数限制功能
private var maxFrameCount: Int64 = 0
private var autoStopCompletion: ((Bool, URL?) -> Void)?
// 池配置
private let maxFrameDataObjects = 15 // 增加池大小
private let maxMetalBuffers = 8
// 调试统计
private var poolHitCount = 0
private var poolMissCount = 0
init(device: MTLDevice) {
self.device = device
self.captureCommandQueue = device.makeCommandQueue()
// 初始化强引用池
frameDataPool = StrongSynchronizedPool<ReusableFrameData>(maxPoolSize: maxFrameDataObjects)
metalBufferPool = StrongSynchronizedPool<MTLBuffer>(maxPoolSize: maxMetalBuffers)
}
deinit {
cleanupResources()
}
func setDelegate(delegate: any VideoRecorderDelegate) {
self.delegate = delegate
}
func configureSize(width: Int, height: Int) {
stateLock.lock()
defer { stateLock.unlock() }
guard !isRecording else {
print("无法在录制中更改尺寸")
return
}
videoWidth = width
videoHeight = height
pixelDataSize = width * height * 4
// 预创建帧数据对象
print("预创建 \(maxFrameDataObjects/2) 个帧数据对象...")
for i in 0..<(maxFrameDataObjects/2) {
let frameData = ReusableFrameData(capacity: pixelDataSize)
do {
_ = try frameDataPool.release(frameData)
print("预创建帧数据对象 \(i+1) 成功")
} catch {
print("预创建帧数据对象 \(i+1) 失败: \(error)")
}
}
// 预创建 Metal 缓冲区
print("预创建 \(maxMetalBuffers/2) 个 Metal 缓冲区...")
for i in 0..<(maxMetalBuffers/2) {
if let buffer = device.makeBuffer(
length: pixelDataSize,
options: .storageModeShared
) {
do {
_ = try metalBufferPool.release(buffer)
print("预创建 Metal 缓冲区 \(i+1) 成功")
} catch {
print("预创建 Metal 缓冲区 \(i+1) 失败: \(error)")
}
}
}
}
func captureFrame(from texture: MTLTexture) {
stateLock.lock()
guard isRecording && !isFinishing else {
stateLock.unlock()
return
}
stateLock.unlock()
let currentTime = CFAbsoluteTimeGetCurrent()
lastCaptureTime = currentTime
let presentationTime: CMTime
if !firstValidFrameCaptured {
presentationTime = .zero
recordingStartTimeAbs = CFAbsoluteTimeGetCurrent()
lastPresentationTime = .zero
recordingStartTime = .zero
firstValidFrameCaptured = true
} else {
presentationTime = calculatePresentationTimeReal()
}
guard let commandQueue = captureCommandQueue else {
print("Metal resources not initialized")
return
}
// 从池中获取 Metal 缓冲区
var metalBuffer: MTLBuffer?
if let poolBuffer = metalBufferPool.acquire() {
metalBuffer = poolBuffer
poolHitCount += 1
} else {
metalBuffer = device.makeBuffer(
length: pixelDataSize,
options: .storageModeShared
)
poolMissCount += 1
if frameCount % 100 == 0 {
print("Metal Buffer Pool - Hits: \(poolHitCount), Misses: \(poolMissCount)")
}
}
guard let buffer = metalBuffer,
let commandBuffer = commandQueue.makeCommandBuffer(),
let blitEncoder = commandBuffer.makeBlitCommandEncoder() else {
if let buf = metalBuffer {
_ = try? metalBufferPool.release(buf)
}
return
}
pendingFrameLock.lock()
pendingFrameCount += 1
pendingFrameLock.unlock()
// 异步复制纹理到缓冲区
blitEncoder.copy(
from: texture,
sourceSlice: 0,
sourceLevel: 0,
sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0),
sourceSize: MTLSize(
width: texture.width,
height: texture.height,
depth: 1
),
to: buffer,
destinationOffset: 0,
destinationBytesPerRow: texture.width * 4,
destinationBytesPerImage: texture.width * texture.height * 4
)
blitEncoder.endEncoding()
// 添加完成回调
commandBuffer.addCompletedHandler { [weak self] _ in
guard let self = self else {
_ = try? self?.metalBufferPool.release(buffer)
return
}
self.stateLock.lock()
let shouldProcess = self.isRecording && !self.isFinishing
self.stateLock.unlock()
if shouldProcess {
// 从池中获取帧数据对象
var frameData: ReusableFrameData?
if let poolData = self.frameDataPool.acquire() {
frameData = poolData
frameData?.reset() // 重置状态
} else {
frameData = ReusableFrameData(capacity: self.pixelDataSize)
print("创建新的 ReusableFrameData,池可能太小")
}
guard let data = frameData else { return }
// 复制数据
data.copyFrom(
buffer: buffer.contents(),
size: self.pixelDataSize,
time: presentationTime,
width: self.videoWidth,
height: self.videoHeight
)
// 将帧数据加入队列
self.linkBlockingQueue.putWithReplace(data) { droppedData in
print("Frame dropped due to queue full")
droppedData.reset()
_ = try? self.frameDataPool.release(droppedData)
}
}
// 回收 Metal 缓冲区
do {
_ = try self.metalBufferPool.release(buffer)
} catch {
print("Metal buffer 回收失败: \(error)")
}
self.pendingFrameLock.lock()
self.pendingFrameCount -= 1
self.pendingFrameLock.unlock()
}
commandBuffer.commit()
}
private func processFrame(_ frameData: ReusableFrameData) {
defer {
// 重要:处理完成后回收到池中
frameData.reset()
do {
let released = try frameDataPool.release(frameData)
if !released {
print("警告:帧数据对象未能回收到池中(池满)")
}
} catch {
print("帧数据回收错误: \(error)")
}
}
guard frameData.isValid,
let assetWriterInput = self.assetWriterInput,
let pixelBufferAdaptor = self.assetWriterPixelBufferAdaptor else {
return
}
// 等待输入准备好
while !assetWriterInput.isReadyForMoreMediaData && isProcessingFrames {
Thread.sleep(forTimeInterval: 0.01)
}
guard assetWriterInput.isReadyForMoreMediaData else { return }
autoreleasepool {
var pixelBuffer: CVPixelBuffer?
// 使用 AVAssetWriter 的像素缓冲池
if let pool = pixelBufferAdaptor.pixelBufferPool {
CVPixelBufferPoolCreatePixelBuffer(
kCFAllocatorDefault,
pool,
&pixelBuffer
)
}
// 如果池创建失败,手动创建
if pixelBuffer == nil {
let attributes = createPixelBufferAttributes()
CVPixelBufferCreate(
kCFAllocatorDefault,
frameData.width,
frameData.height,
kCVPixelFormatType_32BGRA,
attributes as CFDictionary,
&pixelBuffer
)
}
guard let buffer = pixelBuffer else {
print("无法创建像素缓冲区")
return
}
// 复制像素数据
CVPixelBufferLockBaseAddress(buffer, [])
if let baseAddress = CVPixelBufferGetBaseAddress(buffer) {
let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
let dstBuffer = baseAddress.assumingMemoryBound(to: UInt8.self)
copyBGRA(
src: frameData.pixelData,
dst: dstBuffer,
width: frameData.width,
height: frameData.height,
dstBytesPerRow: bytesPerRow
)
}
CVPixelBufferUnlockBaseAddress(buffer, [])
// 写入视频
if pixelBufferAdaptor.append(buffer, withPresentationTime: frameData.presentationTime) {
frameCount += 1
lastPresentationTime = frameData.presentationTime
// 检查帧数限制
if maxFrameCount > 0 && frameCount >= maxFrameCount {
print("达到最大帧数限制: \(maxFrameCount)")
stateLock.lock()
let completion = self.autoStopCompletion
self.autoStopCompletion = nil
stateLock.unlock()
DispatchQueue.main.async { [weak self] in
self?.stopRecording { success, url in
completion?(success, url)
}
}
}
// 定期通知进度
if frameCount % 30 == 0 {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.delegate?.videoRecorderDidCaptureFrame(self, frameCount: self.frameCount)
}
}
}
}
}
private func startFrameProcessing() {
isProcessingFrames = true
thread?.cancel()
thread = YSThread(queue: linkBlockingQueue, name: "YSVideoRecoder2", run: { [weak self] data in
guard let self = self else {
// 如果 self 已释放,仍需要回收数据
data.reset()
return
}
self.stateLock.lock()
let shouldProcess = self.isProcessingFrames
self.stateLock.unlock()
if shouldProcess {
self.processFrame(data)
} else {
// 如果不应该处理,回收数据
data.reset()
_ = try? self.frameDataPool.release(data)
}
})
thread?.start()
}
func startRecording(
outputURL: URL,
playbackSpeed: Double = 1.0,
maxFrames: Int64 = 0,
autoStopCompletion: ((Bool, URL?) -> Void)? = nil
) -> Bool {
stateLock.lock()
defer { stateLock.unlock() }
guard !isRecording else {
print("已经在录制中")
return false
}
guard videoWidth > 0 && videoHeight > 0 else {
print("请先配置视频尺寸")
return false
}
// 重置统计
poolHitCount = 0
poolMissCount = 0
videoSpeedMultiplier = playbackSpeed
maxFrameCount = maxFrames
self.autoStopCompletion = autoStopCompletion
do {
if FileManager.default.fileExists(atPath: outputURL.path) {
try FileManager.default.removeItem(at: outputURL)
}
assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: .mp4)
let videoSettings = createRealVideoSettings()
assetWriterInput = AVAssetWriterInput(
mediaType: .video,
outputSettings: videoSettings
)
assetWriterInput?.expectsMediaDataInRealTime = true
let pixelBufferAttributes = createPixelBufferAttributes()
assetWriterPixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(
assetWriterInput: assetWriterInput!,
sourcePixelBufferAttributes: pixelBufferAttributes
)
if assetWriter!.canAdd(assetWriterInput!) {
assetWriter!.add(assetWriterInput!)
} else {
return false
}
if !assetWriter!.startWriting() {
return false
}
assetWriter!.startSession(atSourceTime: .zero)
isRecording = true
isFinishing = false
firstValidFrameCaptured = false
frameCount = 0
lastCaptureTime = 0
pendingFrameCount = 0
linkBlockingQueue.clear()
linkBlockingQueue.reset()
startFrameProcessing()
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.delegate?.videoRecorderDidStartRecording(self)
}
print("录制开始,池大小 - 帧数据: \(maxFrameDataObjects), Metal缓冲: \(maxMetalBuffers)")
return true
} catch {
print("开始录制失败: \(error)")
return false
}
}
func startRecording(outputURL: URL, playbackSpeed: Double = 1.0) -> Bool {
return startRecording(outputURL: outputURL, playbackSpeed: playbackSpeed, maxFrames: 0, autoStopCompletion: nil)
}
func stopRecording(completion: @escaping (Bool, URL?) -> Void) {
stateLock.lock()
guard isRecording, let assetWriter = self.assetWriter else {
stateLock.unlock()
completion(false, nil)
return
}
isRecording = false
isFinishing = true
let finalFrameCount = self.frameCount
self.autoStopCompletion = nil
// 打印最终统计
print("录制统计 - Pool Hits: \(poolHitCount), Misses: \(poolMissCount)")
stateLock.unlock()
print("正在停止录制,处理剩余帧...")
linkBlockingQueue.interrupt()
recordingQueue.async { [weak self] in
guard let self = self else {
completion(false, nil)
return
}
self.waitForPendingFrames()
self.thread?.cancel()
var waitCount = 0
while self.linkBlockingQueue.size > 0 && waitCount < 100 {
Thread.sleep(forTimeInterval: 0.05)
waitCount += 1
}
while !(self.assetWriterInput?.isReadyForMoreMediaData ?? false) {
Thread.sleep(forTimeInterval: 0.01)
}
self.assetWriterInput?.markAsFinished()
assetWriter.finishWriting {
DispatchQueue.main.async {
let success = assetWriter.status == .completed
let outputURL = success ? assetWriter.outputURL : nil
print("录制完成 - 状态: \(success ? "成功" : "失败"), 总帧数: \(finalFrameCount)")
self.cleanupRecordingResources()
self.delegate?.videoRecorderDidStopRecording(
self,
success: success,
outputURL: outputURL
)
completion(success, outputURL)
}
}
}
}
func isDecodering() -> Bool {
stateLock.lock()
defer { stateLock.unlock() }
return isRecording
}
func getCurrentFrameCount() -> Int64 {
stateLock.lock()
defer { stateLock.unlock() }
return frameCount
}
func getMaxFrameCount() -> Int64 {
stateLock.lock()
defer { stateLock.unlock() }
return maxFrameCount
}
private func waitForPendingFrames() {
var waitCount = 0
while waitCount < 100 {
pendingFrameLock.lock()
let count = pendingFrameCount
pendingFrameLock.unlock()
if count == 0 {
break
}
Thread.sleep(forTimeInterval: 0.05)
waitCount += 1
}
}
private func calculatePresentationTimeReal() -> CMTime {
let currentTimeAbs = CFAbsoluteTimeGetCurrent()
let elapsedTime = currentTimeAbs - recordingStartTimeAbs
let adjustedTime = elapsedTime * videoSpeedMultiplier
return CMTime(seconds: adjustedTime, preferredTimescale: 600)
}
private func cleanupRecordingResources() {
stateLock.lock()
defer { stateLock.unlock() }
isProcessingFrames = false
isFinishing = false
assetWriter = nil
assetWriterInput = nil
assetWriterPixelBufferAdaptor = nil
recordingStartTime = nil
recordingStartTimeAbs = 0
frameCount = 0
lastPresentationTime = .zero
pendingFrameCount = 0
maxFrameCount = 0
autoStopCompletion = nil
linkBlockingQueue.clear()
thread?.cancel()
thread = nil
}
private func cleanupResources() {
cleanupRecordingResources()
frameDataPool = nil
metalBufferPool = nil
}
private func copyBGRA(
src: UnsafePointer<UInt8>,
dst: UnsafeMutablePointer<UInt8>,
width: Int,
height: Int,
dstBytesPerRow: Int
) {
let srcBytesPerRow = width * 4
for y in 0..<height {
let srcRowStart = y * srcBytesPerRow
let dstRowStart = y * dstBytesPerRow
memcpy(dst + dstRowStart, src + srcRowStart, srcBytesPerRow)
}
}
private func createRealVideoSettings() -> [String: Any] {
return [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: videoWidth,
AVVideoHeightKey: videoHeight,
AVVideoCompressionPropertiesKey: [
AVVideoAverageBitRateKey: NSNumber(value: videoWidth * videoHeight * 10),
AVVideoProfileLevelKey: AVVideoProfileLevelH264High41,
AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC,
],
]
}
private func createPixelBufferAttributes() -> [String: Any] {
return [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
kCVPixelBufferWidthKey as String: videoWidth,
kCVPixelBufferHeightKey as String: videoHeight,
kCVPixelBufferOpenGLESCompatibilityKey as String: true,
kCVPixelBufferIOSurfacePropertiesKey as String: [:],
kCVPixelBufferCGImageCompatibilityKey as String: true,
kCVPixelBufferCGBitmapContextCompatibilityKey as String: true,
]
}
}
//
// Pool.swift
// wscanner
//
// Created by yunshen on 2025/2/17.
//
import Foundation
/// 定义池操作可能抛出的错误
public enum PoolError: Error {
case alreadyInPool
}
/// 定义对象池协议
public protocol Pool {
associatedtype Element: AnyObject
/// 从池中获取一个对象,如果池中没有则返回 nil
func acquire() -> Element?
/// 将一个对象放回池中,如果对象已存在则抛出异常,放回成功返回 true,否则返回 false
func release(_ instance: Element) throws -> Bool
}
/// 非线程安全的简单对象池实现
//public class SimplePool<T: AnyObject>: Pool {
// public typealias Element = T
// /// 池中保存的对象集合(后进先出)
// private var pool: [T]
// /// 池的最大容量(池中可保存的对象种类数)
// private let maxPoolSize: Int
//
// /// 构造方法,maxPoolSize 必须大于 0
// public init(maxPoolSize: Int) {
// precondition(maxPoolSize > 0, "maxPoolSize must be > 0")
// self.maxPoolSize = maxPoolSize
// self.pool = []
// }
//
// /// 获取一个对象(后进先出)
// public func acquire() -> T? {
// if !pool.isEmpty {
// return pool.removeLast()
// }
// return nil
// }
//
// /// 将对象放回池中,如果该对象已存在则抛出 PoolError.alreadyInPool 异常,
// /// 如果池中未满则返回 true,否则返回 false(不放入池中)。
// public func release(_ instance: T) throws -> Bool {
// if pool.contains(where: { $0 === instance }) {
// throw PoolError.alreadyInPool
// }
// if pool.count < maxPoolSize {
// pool.append(instance)
// return true
// }
// return false
// }
//}
//// 弱引用包装类
//private class WeakRef<T: AnyObject> {
// weak var value: T?
// init(value: T) { self.value = value }
//}
//
//public class SimplePool<T: AnyObject>: Pool {
// public typealias Element = T
// private var pool: [WeakRef<T>]
// private let maxPoolSize: Int
//
// public init(maxPoolSize: Int) {
// precondition(maxPoolSize > 0, "maxPoolSize must be > 0")
// self.maxPoolSize = maxPoolSize
// self.pool = []
// }
//
// public func acquire() -> T? {
// // while !pool.isEmpty {
// // if let instance = pool.removeLast().value {
// // return instance
// // }
// // }
// // return nil
// pool = pool.filter { $0.value != nil } // 清理所有空引用
// return pool.popLast()?.value // 直接取最后一个有效对象
// }
//
// public func release(_ instance: T) throws -> Bool {
// if pool.contains(where: { $0.value === instance }) {
// throw PoolError.alreadyInPool
// }
// if pool.count < maxPoolSize {
// pool.append(WeakRef(value: instance))
// return true
// }
// return false
// }
//}
public class SimplePool<T: AnyObject>: Pool {
public typealias Element = T
private var pool: Set<WeakRef<T>>
private let maxPoolSize: Int
public init(maxPoolSize: Int) {
precondition(maxPoolSize > 0, "maxPoolSize must be > 0")
self.maxPoolSize = maxPoolSize
self.pool = []
}
public func acquire() -> T? {
while let weakRef = pool.popFirst() {
if let value = weakRef.value {
return value
}
pool = pool.filter( { $0.value != nil } )
}
return nil
}
public func release(_ instance: T) throws -> Bool {
let weakRef = WeakRef(value: instance)
if pool.contains(where: { $0.value === instance }) {
throw PoolError.alreadyInPool
}
if pool.count < maxPoolSize {
pool.insert(weakRef)
return true
}
return false
}
}
private class WeakRef<T: AnyObject>: Hashable {
weak var value: T?
private let identifier: ObjectIdentifier
init(value: T) {
self.value = value
self.identifier = ObjectIdentifier(value)
}
static func == (lhs: WeakRef<T>, rhs: WeakRef<T>) -> Bool {
lhs.identifier == rhs.identifier
}
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
}
public class SynchronizedPool<T: AnyObject>: SimplePool<T> {
private let lock = NSLock()
public override func acquire() -> T? {
lock.lock()
defer {
lock.unlock()
}
return super.acquire()
}
public override func release(_ instance: T) throws -> Bool {
lock.lock()
defer {
lock.unlock()
}
return try super.release(instance)
}
}
public class StrongSimplePool<T: AnyObject>: Pool {
public typealias Element = T
private var pool: [T]
private let maxPoolSize: Int
public init(maxPoolSize: Int) {
precondition(maxPoolSize > 0, "maxPoolSize must be > 0")
self.maxPoolSize = maxPoolSize
self.pool = []
}
public func acquire() -> T? {
if !pool.isEmpty {
return pool.removeLast()
}
return nil
}
public func release(_ instance: T) throws -> Bool {
if pool.contains(where: { $0 === instance }) {
throw PoolError.alreadyInPool
}
if pool.count < maxPoolSize {
pool.append(instance)
return true
}
return false
}
}
// MARK: - 线程安全的强引用池
public class StrongSynchronizedPool<T: AnyObject>: StrongSimplePool<T> {
private let lock = NSLock()
public override func acquire() -> T? {
lock.lock()
defer { lock.unlock() }
return super.acquire()
}
public override func release(_ instance: T) throws -> Bool {
lock.lock()
defer { lock.unlock() }
return try super.release(instance)
}
}
//
// YSVideoRecoder2.swift
// wscanner
//
// Created by yunshen on 2025/9/5.
//
import AVFoundation
import Foundation
import MetalKit
internal import common
protocol OnRecordFinishDelegate: AnyObject {
func onRecordFinish(isSuccess: Bool, outPath: URL?)
func onRecordStart()
func onRecordProgress(progress: CGFloat)
func onRecordFrameIndex(index: Int)
}
protocol IRecoder: AnyObject {
var frameCount: Int64 { get }
func setDelegate(delegate: OnRecordFinishDelegate)
func configureSize(width: Int, height: Int)
func startRecording(
outputURL: URL,
playbackSpeed: Double,
maxFrames: Int64
)
func stopRecording()
func captureFrame(from texture: MTLTexture)
func isDecodering() -> Bool
}
class ReusableFrameData {
var pixelData: UnsafeMutablePointer<UInt8>
let capacity: Int
var presentationTime: CMTime = .zero
var width: Int = 0
var height: Int = 0
var isValid: Bool = false
init(capacity: Int) {
self.capacity = capacity
self.pixelData = UnsafeMutablePointer<UInt8>.allocate(
capacity: capacity
)
print("ReusableFrameData init - capacity: \(capacity)")
}
deinit {
pixelData.deallocate()
print("ReusableFrameData deinit - 不应该频繁出现此消息")
}
func reset() {
isValid = false
presentationTime = .zero
width = 0
height = 0
}
func copyFrom(
buffer: UnsafeRawPointer,
size: Int,
time: CMTime,
width: Int,
height: Int
) {
guard size <= capacity else {
print("Error: size \(size) > capacity \(capacity)")
return
}
memcpy(pixelData, buffer, size)
self.presentationTime = time
self.width = width
self.height = height
self.isValid = true
}
}
// MARK: - 录制状态
private enum RecorderState {
case idle
case preparing
case recording
case stopping
case stopped
}
// MARK: - 优化后的 YSVideoRecoder
class YSVideoRecoder: IRecoder {
// MARK: - Properties
private weak var delegate: OnRecordFinishDelegate?
private let device: MTLDevice
private var captureCommandQueue: MTLCommandQueue?
// 使用并发队列替代锁,避免死锁
private let stateQueue = DispatchQueue(
label: "com.videorecorder.state",
attributes: .concurrent
)
private let recordingQueue = DispatchQueue(
label: "com.videorecorder.recording",
qos: .userInitiated
)
// 状态管理(私有,通过 stateQueue 访问)
private var _state: RecorderState = .idle
private var _frameCount: Int64 = 0
private var _videoWidth: Int = 0
private var _videoHeight: Int = 0
private var _maxFrameCount: Int64 = 0
// 配置
private var videoSpeedMultiplier: Double = 1.0
private var pixelDataSize: Int = 0
// AVAssetWriter 相关
private var assetWriter: AVAssetWriter?
private var assetWriterInput: AVAssetWriterInput?
private var assetWriterPixelBufferAdaptor:
AVAssetWriterInputPixelBufferAdaptor?
// 时间管理
private var recordingStartTimeAbs: CFAbsoluteTime = 0
private var lastPresentationTime: CMTime = .zero
private var firstValidFrameCaptured: Bool = false
// 帧处理
private var frameDataPool: StrongSynchronizedPool<ReusableFrameData>!
private var metalBufferPool: StrongSynchronizedPool<MTLBuffer>!
private var frameQueue = LinkedBlockingQueue<ReusableFrameData>(
capacity: 100, // 降低到100,避免内存占用过大
target: "YSVideoRecoder",
isPrint: false
)
private var frameProcessingThread: YSThread<ReusableFrameData>?
// 性能监控
private var performanceMonitor = PerformanceMonitor()
// 池配置(优化后的值)
private let maxFrameDataObjects = 10 // 降低池大小,够用即可
private let maxMetalBuffers = 6
// MARK: - Computed Properties (线程安全)
private var state: RecorderState {
get { stateQueue.sync { _state } }
set { stateQueue.async(flags: .barrier) { self._state = newValue } }
}
var frameCount: Int64 {
stateQueue.sync { _frameCount }
}
var isRecording: Bool {
stateQueue.sync { _state == .recording }
}
// MARK: - Initialization
init(device: MTLDevice) {
self.device = device
self.captureCommandQueue = device.makeCommandQueue()
setupResourcePools()
}
deinit {
cleanup()
}
// MARK: - Setup
private func setupResourcePools() {
frameDataPool = StrongSynchronizedPool<ReusableFrameData>(
maxPoolSize: maxFrameDataObjects
)
metalBufferPool = StrongSynchronizedPool<MTLBuffer>(
maxPoolSize: maxMetalBuffers
)
}
// MARK: - Public Methods
func setDelegate(delegate: any OnRecordFinishDelegate) {
self.delegate = delegate
}
func configureSize(width: Int, height: Int) {
stateQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
guard self._state == .idle else {
print("⚠️ 无法在录制中更改尺寸")
return
}
self._videoWidth = width
self._videoHeight = height
self.pixelDataSize = width * height * 4
// 可选:预热资源池
self.recordingQueue.async {
self.warmupPools()
}
}
}
func startRecording(
outputURL: URL,
playbackSpeed: Double = 1.0,
maxFrames: Int64 = 0
) {
// 异步执行,不阻塞调用线程
recordingQueue.async { [weak self] in
self?.performStartRecording(
outputURL: outputURL,
playbackSpeed: playbackSpeed,
maxFrames: maxFrames
)
}
}
func stopRecording() {
// 更新状态
let wasRecording = stateQueue.sync { () -> Bool in
guard _state == .recording else { return false }
return true
}
guard wasRecording else {
notifyCompletion(success: false, url: nil)
return
}
stateQueue.async(flags: .barrier) {
self._state = .stopping
}
// 异步停止
recordingQueue.async { [weak self] in
self?.performStopRecording()
}
}
func captureFrame(from texture: MTLTexture) {
// 快速检查状态,不阻塞
guard stateQueue.sync(execute: { _state == .recording }) else { return }
// 性能监控
performanceMonitor.frameStart()
// 计算时间戳
let presentationTime = calculatePresentationTime()
// 获取或创建 Metal 缓冲区
let metalBuffer = getOrCreateMetalBuffer()
guard let buffer = metalBuffer,
let commandBuffer = captureCommandQueue?.makeCommandBuffer(),
let blitEncoder = commandBuffer.makeBlitCommandEncoder()
else {
recycleMetalBuffer(metalBuffer)
return
}
// 配置 GPU 命令
configureBlit(encoder: blitEncoder, from: texture, to: buffer)
blitEncoder.endEncoding()
// 异步处理完成的帧
commandBuffer.addCompletedHandler { [weak self] _ in
self?.handleCapturedFrame(
buffer: buffer,
presentationTime: presentationTime
)
}
commandBuffer.commit()
}
// MARK: - Private Methods
private func performStartRecording(
outputURL: URL,
playbackSpeed: Double,
maxFrames: Int64
) {
// 检查状态
let canStart = stateQueue.sync { () -> (Bool, Int, Int) in
guard _state == .idle else {
print("⚠️ 当前状态不允许开始录制: \(_state)")
return (false, 0, 0)
}
guard _videoWidth > 0 && _videoHeight > 0 else {
print("⚠️ 请先配置视频尺寸")
return (false, 0, 0)
}
_state = .preparing
return (true, _videoWidth, _videoHeight)
}
guard canStart.0 else {
notifyCompletion(success: false, url: nil)
return
}
// 准备录制
videoSpeedMultiplier = playbackSpeed
stateQueue.async(flags: .barrier) {
self._maxFrameCount = maxFrames
self._frameCount = 0
}
// 设置 AVAssetWriter
guard
setupAssetWriter(
outputURL: outputURL,
width: canStart.1,
height: canStart.2
)
else {
stateQueue.async(flags: .barrier) { self._state = .idle }
notifyCompletion(success: false, url: nil)
return
}
// 重置状态
resetRecordingState()
// 启动帧处理线程
startFrameProcessing()
// 更新状态为录制中
stateQueue.async(flags: .barrier) {
self._state = .recording
}
// 通知开始
DispatchQueue.main.async { [weak self] in
self?.delegate?.onRecordStart()
}
print("✅ 录制开始")
}
private func performStopRecording() {
print("⏹ 正在停止录制...")
// 停止接收新帧
frameQueue.interrupt()
// 等待队列清空
waitForQueueEmpty(timeout: 5.0)
// 停止处理线程
frameProcessingThread?.cancel()
frameProcessingThread = nil
// 完成写入
finishAssetWriting()
// 清理资源
cleanup()
// 更新状态
stateQueue.async(flags: .barrier) {
self._state = .idle
}
print("✅ 录制停止完成")
}
private func setupAssetWriter(outputURL: URL, width: Int, height: Int)
-> Bool
{
do {
// 删除已存在的文件
if FileManager.default.fileExists(atPath: outputURL.path) {
try FileManager.default.removeItem(at: outputURL)
}
// 创建 writer
assetWriter = try AVAssetWriter(
outputURL: outputURL,
fileType: .mp4
)
// 视频设置
let videoSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: width,
AVVideoHeightKey: height,
AVVideoCompressionPropertiesKey: [
AVVideoAverageBitRateKey: width * height * 10,
AVVideoProfileLevelKey: AVVideoProfileLevelH264High41,
AVVideoMaxKeyFrameIntervalKey: 30,
AVVideoExpectedSourceFrameRateKey: 30,
],
]
// 创建输入
assetWriterInput = AVAssetWriterInput(
mediaType: .video,
outputSettings: videoSettings
)
assetWriterInput?.expectsMediaDataInRealTime = true
// 像素缓冲适配器
let pixelBufferAttributes: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String:
kCVPixelFormatType_32BGRA,
kCVPixelBufferWidthKey as String: width,
kCVPixelBufferHeightKey as String: height,
kCVPixelBufferIOSurfacePropertiesKey as String: [:],
kCVPixelBufferMetalCompatibilityKey as String: true,
]
assetWriterPixelBufferAdaptor =
AVAssetWriterInputPixelBufferAdaptor(
assetWriterInput: assetWriterInput!,
sourcePixelBufferAttributes: pixelBufferAttributes
)
// 添加输入并开始
guard let writer = assetWriter,
let input = assetWriterInput,
writer.canAdd(input)
else {
return false
}
writer.add(input)
guard writer.startWriting() else {
print("⚠️ AVAssetWriter startWriting 失败")
return false
}
writer.startSession(atSourceTime: .zero)
return true
} catch {
print("⚠️ 设置 AVAssetWriter 失败: \(error)")
return false
}
}
private func startFrameProcessing() {
frameQueue.clear()
frameQueue.reset()
frameProcessingThread = YSThread(
queue: frameQueue,
name: "FrameProcessor",
run: { [weak self] frameData in
self?.processFrame(frameData)
},
timeout: 30,
onTimeout: { [weak self] in
print("⚠️ 帧处理超时")
self?.stopRecording()
}
)
frameProcessingThread?.start()
}
private func processFrame(_ frameData: ReusableFrameData) {
defer {
frameData.reset()
_ = try? frameDataPool.release(frameData)
}
guard frameData.isValid,
let input = assetWriterInput,
let adaptor = assetWriterPixelBufferAdaptor
else {
return
}
// 等待输入准备好
waitForInputReady(input: input, timeout: 0.1)
guard input.isReadyForMoreMediaData else { return }
// 创建像素缓冲区并写入
autoreleasepool {
if let pixelBuffer = createPixelBuffer(from: frameData) {
if adaptor.append(
pixelBuffer,
withPresentationTime: frameData.presentationTime
) {
incrementFrameCount()
checkFrameLimit()
}
}
}
}
private func handleCapturedFrame(
buffer: MTLBuffer,
presentationTime: CMTime
) {
defer {
recycleMetalBuffer(buffer)
}
// 检查状态
guard stateQueue.sync(execute: { _state == .recording }) else { return }
// 获取或创建帧数据对象
guard let frameData = getOrCreateFrameData() else { return }
// 复制数据
frameData.copyFrom(
buffer: buffer.contents(),
size: pixelDataSize,
time: presentationTime,
width: stateQueue.sync { _videoWidth },
height: stateQueue.sync { _videoHeight }
)
// 加入队列
frameQueue.putWithReplace(frameData) { droppedData in
print("丢帧")
droppedData.reset()
_ = try? self.frameDataPool.release(droppedData)
}
performanceMonitor.frameEnd()
}
// MARK: - Helper Methods
private func calculatePresentationTime() -> CMTime {
if !firstValidFrameCaptured {
firstValidFrameCaptured = true
recordingStartTimeAbs = CFAbsoluteTimeGetCurrent()
return .zero
}
let elapsed = CFAbsoluteTimeGetCurrent() - recordingStartTimeAbs
let adjusted = elapsed * videoSpeedMultiplier
return CMTime(seconds: adjusted, preferredTimescale: 600)
}
private func incrementFrameCount() {
stateQueue.async(flags: .barrier) {
self._frameCount += 1
// 定期通知进度
if self._frameCount % 2 == 0 {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let count = self.frameCount
self.delegate?.onRecordFrameIndex(index: Int(count))
if self._maxFrameCount > 0 {
let progress =
CGFloat(count) / CGFloat(self._maxFrameCount)
self.delegate?.onRecordProgress(progress: progress)
}
}
}
}
}
private func checkFrameLimit() {
let (count, max) = stateQueue.sync { (_frameCount, _maxFrameCount) }
if max > 0 && count >= max {
print("📊 达到最大帧数: \(max)")
DispatchQueue.main.async { [weak self] in
self?.stopRecording()
}
}
}
private func getOrCreateMetalBuffer() -> MTLBuffer? {
if let buffer = metalBufferPool.acquire() {
return buffer
}
return device.makeBuffer(
length: pixelDataSize,
options: .storageModeShared
)
}
private func recycleMetalBuffer(_ buffer: MTLBuffer?) {
guard let buffer = buffer else { return }
_ = try? metalBufferPool.release(buffer)
}
private func getOrCreateFrameData() -> ReusableFrameData? {
if let data = frameDataPool.acquire() {
data.reset()
return data
}
return ReusableFrameData(capacity: pixelDataSize)
}
private func createPixelBuffer(from frameData: ReusableFrameData)
-> CVPixelBuffer?
{
var pixelBuffer: CVPixelBuffer?
// 尝试从池中获取
if let pool = assetWriterPixelBufferAdaptor?.pixelBufferPool {
CVPixelBufferPoolCreatePixelBuffer(
kCFAllocatorDefault,
pool,
&pixelBuffer
)
}
// 如果失败,手动创建
if pixelBuffer == nil {
let attrs: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String:
kCVPixelFormatType_32BGRA,
kCVPixelBufferWidthKey as String: frameData.width,
kCVPixelBufferHeightKey as String: frameData.height,
]
CVPixelBufferCreate(
kCFAllocatorDefault,
frameData.width,
frameData.height,
kCVPixelFormatType_32BGRA,
attrs as CFDictionary,
&pixelBuffer
)
}
guard let buffer = pixelBuffer else { return nil }
// 复制数据
CVPixelBufferLockBaseAddress(buffer, [])
defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
if let baseAddress = CVPixelBufferGetBaseAddress(buffer) {
let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
let dstBuffer = baseAddress.assumingMemoryBound(to: UInt8.self)
copyBGRA(
src: frameData.pixelData,
dst: dstBuffer,
width: frameData.width,
height: frameData.height,
dstBytesPerRow: bytesPerRow
)
}
return buffer
}
private func copyBGRA(
src: UnsafePointer<UInt8>,
dst: UnsafeMutablePointer<UInt8>,
width: Int,
height: Int,
dstBytesPerRow: Int
) {
let srcBytesPerRow = width * 4
for y in 0..<height {
let srcRowStart = y * srcBytesPerRow
let dstRowStart = y * dstBytesPerRow
memcpy(dst + dstRowStart, src + srcRowStart, srcBytesPerRow)
}
}
private func configureBlit(
encoder: MTLBlitCommandEncoder,
from texture: MTLTexture,
to buffer: MTLBuffer
) {
encoder.copy(
from: texture,
sourceSlice: 0,
sourceLevel: 0,
sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0),
sourceSize: MTLSize(
width: texture.width,
height: texture.height,
depth: 1
),
to: buffer,
destinationOffset: 0,
destinationBytesPerRow: texture.width * 4,
destinationBytesPerImage: texture.width * texture.height * 4
)
}
private func waitForInputReady(
input: AVAssetWriterInput,
timeout: TimeInterval
) {
let deadline = Date().addingTimeInterval(timeout)
while !input.isReadyForMoreMediaData && Date() < deadline {
Thread.sleep(forTimeInterval: 0.001)
}
}
private func waitForQueueEmpty(timeout: TimeInterval) {
let deadline = Date().addingTimeInterval(timeout)
while frameQueue.size > 0 && Date() < deadline {
Thread.sleep(forTimeInterval: 0.01)
}
}
private func finishAssetWriting() {
assetWriterInput?.markAsFinished()
let semaphore = DispatchSemaphore(value: 0)
var success = false
var outputURL: URL?
assetWriter?.finishWriting { [weak self] in
success = (self?.assetWriter?.status == .completed)
outputURL = success ? self?.assetWriter?.outputURL : nil
semaphore.signal()
}
_ = semaphore.wait(timeout: .now() + 5.0)
notifyCompletion(success: success, url: outputURL)
}
private func warmupPools() {
// 预创建少量资源
for _ in 0..<2 {
if let data = ReusableFrameData(capacity: pixelDataSize)
as ReusableFrameData?
{
_ = try? frameDataPool.release(data)
}
if let buffer = device.makeBuffer(
length: pixelDataSize,
options: .storageModeShared
) {
_ = try? metalBufferPool.release(buffer)
}
}
}
private func resetRecordingState() {
firstValidFrameCaptured = false
recordingStartTimeAbs = 0
lastPresentationTime = .zero
performanceMonitor.reset()
}
private func cleanup() {
frameQueue.clear()
frameProcessingThread?.cancel()
frameProcessingThread = nil
assetWriter = nil
assetWriterInput = nil
assetWriterPixelBufferAdaptor = nil
resetRecordingState()
}
private func notifyCompletion(success: Bool, url: URL?) {
DispatchQueue.main.async { [weak self] in
self?.delegate?.onRecordFinish(isSuccess: success, outPath: url)
}
}
// MARK: - IRecoder Protocol
func isDecodering() -> Bool {
return isRecording
}
func getCurrentFrameCount() -> Int64 {
return frameCount
}
func getMaxFrameCount() -> Int64 {
return stateQueue.sync { _maxFrameCount }
}
}
// MARK: - 性能监控器
private class PerformanceMonitor {
private var frameStartTime: CFAbsoluteTime = 0
private var frameTimes: [TimeInterval] = []
private let maxSamples = 50
func frameStart() {
frameStartTime = CFAbsoluteTimeGetCurrent()
}
func frameEnd() {
let duration = CFAbsoluteTimeGetCurrent() - frameStartTime
frameTimes.append(duration)
if frameTimes.count > maxSamples {
frameTimes.removeFirst()
}
// 定期输出性能统计
if frameTimes.count == maxSamples {
let avg = frameTimes.reduce(0, +) / Double(frameTimes.count) * 1000
// let fps = 1.0 / avg
print("平均 毫秒: \(avg.description)")
frameTimes.removeAll()
}
}
func reset() {
frameTimes.removeAll()
frameStartTime = 0
}
}
Giscus 未启用:请在
src/site.config.ts中配置 repoId 与 categoryId。