fix: MTAudioProcessingTapCallbacks参数名initCallback→init
修复编译错误:Swift bridging 期望的参数名是 init 而非 initCallback
This commit is contained in:
parent
5ae4cc4e70
commit
d9f12ced06
@ -1,50 +1,43 @@
|
||||
import AVFoundation
|
||||
import CoreMedia
|
||||
import MediaToolbox
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
|
||||
// MARK: - 音频捕获上下文(通过 tapStorage 传给 C 回调)
|
||||
// MARK: - 音频捕获上下文
|
||||
class AudioCaptureContext {
|
||||
var writerInput: AVAssetWriterInput?
|
||||
nonisolated(unsafe) var isRunning: Bool = true
|
||||
nonisolated(unsafe) var formatDescription: CMAudioFormatDescription?
|
||||
nonisolated(unsafe) var appendCount: Int = 0
|
||||
nonisolated(unsafe) var formatDescription: CMAudioFormatDescription?
|
||||
nonisolated(unsafe) var firstAppendLogged: Bool = false
|
||||
}
|
||||
|
||||
// MARK: - MTAudioProcessingTap C 回调函数
|
||||
|
||||
private func tapInit(_ tap: MTAudioProcessingTap,
|
||||
_ clientInfo: UnsafeMutableRawPointer?,
|
||||
_ tapStorageOut: UnsafeMutablePointer<UnsafeMutableRawPointer?>) {
|
||||
// 将 clientInfo 存入 tapStorage
|
||||
tapStorageOut.pointee = clientInfo
|
||||
}
|
||||
|
||||
private func tapFinalize(_ tap: MTAudioProcessingTap) {
|
||||
// 释放 context 的 Unmanaged 引用
|
||||
guard let storage = Optional(MTAudioProcessingTapGetStorage(tap)),
|
||||
let ctxPtr = storage.assumingMemoryBound(to: UnsafeMutableRawPointer?.self).pointee else { return }
|
||||
Unmanaged<AudioCaptureContext>.fromOpaque(ctxPtr).release()
|
||||
}
|
||||
/// 全局引用,供 C 回调访问(单实例录制,线程安全由 isRunning 保护)
|
||||
private nonisolated(unsafe) var gAudioContext: AudioCaptureContext?
|
||||
|
||||
// MARK: - MTAudioProcessingTap C 回调
|
||||
private func tapPrepare(_ tap: MTAudioProcessingTap,
|
||||
_ maxFrames: Int,
|
||||
_ maxFramesPerSlice: CMItemCount,
|
||||
_ processingFormat: UnsafePointer<AudioStreamBasicDescription>) {
|
||||
guard let storage = Optional(MTAudioProcessingTapGetStorage(tap)),
|
||||
let ctxPtr = storage.assumingMemoryBound(to: UnsafeMutableRawPointer?.self).pointee else { return }
|
||||
let ctx = Unmanaged<AudioCaptureContext>.fromOpaque(ctxPtr).takeUnretainedValue()
|
||||
|
||||
print("[Recorder] tapPrepare called, maxFrames=\(maxFramesPerSlice)")
|
||||
guard let ctx = gAudioContext else {
|
||||
print("[Recorder] tapPrepare: gAudioContext is nil!")
|
||||
return
|
||||
}
|
||||
var asbd = processingFormat.pointee
|
||||
var fmtDesc: CMAudioFormatDescription?
|
||||
print("[Recorder] tapPrepare: sampleRate=\(asbd.mSampleRate), channels=\(asbd.mChannelsPerFrame)")
|
||||
CMAudioFormatDescriptionCreate(allocator: kCFAllocatorDefault,
|
||||
asbd: &asbd,
|
||||
layoutSize: 0, layout: nil,
|
||||
magicCookieSize: 0, magicCookie: nil,
|
||||
extensions: nil,
|
||||
formatDescriptionOut: &fmtDesc)
|
||||
ctx.formatDescription = fmtDesc
|
||||
print("[Recorder] Audio tap prepared: \(asbd.mSampleRate)Hz, \(asbd.mChannelsPerFrame)ch, format=\(asbd.mFormatID)")
|
||||
formatDescriptionOut: &ctx.formatDescription)
|
||||
}
|
||||
|
||||
private func tapInit(_ tap: MTAudioProcessingTap,
|
||||
_ clientInfo: UnsafeMutableRawPointer?,
|
||||
_ tapStorageOut: UnsafeMutablePointer<UnsafeMutableRawPointer?>) {
|
||||
// 初始化 tap
|
||||
}
|
||||
|
||||
private func tapUnprepare(_ tap: MTAudioProcessingTap) {
|
||||
@ -52,7 +45,7 @@ private func tapUnprepare(_ tap: MTAudioProcessingTap) {
|
||||
}
|
||||
|
||||
private func tapProcess(_ tap: MTAudioProcessingTap,
|
||||
_ numberOfFrames: CMItemCount,
|
||||
_ numberFrames: CMItemCount,
|
||||
_ flags: MTAudioProcessingTapFlags,
|
||||
_ bufferListInOut: UnsafeMutablePointer<AudioBufferList>,
|
||||
_ numberFramesOut: UnsafeMutablePointer<CMItemCount>,
|
||||
@ -62,18 +55,14 @@ private func tapProcess(_ tap: MTAudioProcessingTap,
|
||||
|
||||
guard numberFrames > 0 else { return }
|
||||
|
||||
// 获取源音频数据 + 时间范围
|
||||
// 先获取源音频数据
|
||||
var timeRange = CMTimeRange()
|
||||
var srcFlags: UInt32 = 0
|
||||
var actualFrames: Int = 0
|
||||
var srcFlags: MTAudioProcessingTapFlags = 0
|
||||
var actualFrames: CMItemCount = 0
|
||||
|
||||
let status = MTAudioProcessingTapGetSourceAudio(
|
||||
tap,
|
||||
numberFrames,
|
||||
bufferListInOut,
|
||||
&srcFlags,
|
||||
&timeRange,
|
||||
&actualFrames
|
||||
tap, numberFrames, bufferListInOut,
|
||||
&srcFlags, &timeRange, &actualFrames
|
||||
)
|
||||
|
||||
numberFramesOut.pointee = actualFrames
|
||||
@ -81,71 +70,71 @@ private func tapProcess(_ tap: MTAudioProcessingTap,
|
||||
|
||||
guard status == noErr, actualFrames > 0 else { return }
|
||||
|
||||
// 获取 context
|
||||
guard let storage = Optional(MTAudioProcessingTapGetStorage(tap)),
|
||||
let ctxPtr = storage.assumingMemoryBound(to: UnsafeMutableRawPointer?.self).pointee else { return }
|
||||
let ctx = Unmanaged<AudioCaptureContext>.fromOpaque(ctxPtr).takeUnretainedValue()
|
||||
|
||||
guard ctx.isRunning, let writerInput = ctx.writerInput,
|
||||
// 通过全局变量获取 context
|
||||
guard let ctx = gAudioContext, ctx.isRunning,
|
||||
let writerInput = ctx.writerInput,
|
||||
writerInput.isReadyForMoreMediaData else { return }
|
||||
|
||||
let bufferList = bufferListInOut.pointee
|
||||
guard bufferList.mNumberBuffers > 0 else { return }
|
||||
|
||||
// 计算总字节数
|
||||
var totalBytes: UInt32 = 0
|
||||
for i in 0..<Int(bufferList.mNumberBuffers) {
|
||||
let buf = bufferList.mBuffers // Note: only single buffer for interleaved
|
||||
totalBytes = buf.mDataByteSize
|
||||
break
|
||||
}
|
||||
guard totalBytes > 0, let srcData = bufferList.mBuffers.mData else { return }
|
||||
let buf = bufferList.mBuffers
|
||||
let dataSize = Int(buf.mDataByteSize)
|
||||
guard dataSize > 0, let srcData = buf.mData else { return }
|
||||
|
||||
// 创建 CMBlockBuffer
|
||||
var blockBuffer: CMBlockBuffer?
|
||||
guard CMBlockBufferCreateWithMemoryBlock(
|
||||
allocator: kCFAllocatorDefault,
|
||||
memoryBlock: nil,
|
||||
blockLength: Int(totalBytes),
|
||||
blockAllocator: kCFAllocatorDefault,
|
||||
customBlockSource: nil,
|
||||
offsetToData: 0,
|
||||
dataLength: Int(totalBytes),
|
||||
let blockStatus = CMBlockBufferCreateWithMemoryBlock(
|
||||
allocator: kCFAllocatorDefault, memoryBlock: nil,
|
||||
blockLength: dataSize, blockAllocator: kCFAllocatorDefault,
|
||||
customBlockSource: nil, offsetToData: 0,
|
||||
dataLength: dataSize,
|
||||
flags: kCMBlockBufferAssureMemoryNowFlag,
|
||||
blockBufferOut: &blockBuffer
|
||||
) == kCMBlockBufferNoErr, let bb = blockBuffer else { return }
|
||||
)
|
||||
guard blockStatus == kCMBlockBufferNoErr, let bb = blockBuffer else { return }
|
||||
|
||||
// 复制音频数据
|
||||
guard CMBlockBufferReplaceDataBytes(
|
||||
with: srcData,
|
||||
blockBuffer: bb,
|
||||
offsetIntoDestination: 0,
|
||||
dataLength: Int(totalBytes)
|
||||
) == kCMBlockBufferNoErr else { return }
|
||||
let replaceStatus = CMBlockBufferReplaceDataBytes(
|
||||
with: srcData, blockBuffer: bb,
|
||||
offsetIntoDestination: 0, dataLength: dataSize
|
||||
)
|
||||
guard replaceStatus == kCMBlockBufferNoErr else { return }
|
||||
|
||||
// 获取格式描述
|
||||
guard let fmtDesc = ctx.formatDescription else { return }
|
||||
// 获取 format description
|
||||
guard let fd = ctx.formatDescription else { return }
|
||||
|
||||
// 用累积帧数计算 PTS
|
||||
let sampleRate = Double(timeRange.duration.timescale) > 0
|
||||
? Double(timeRange.duration.timescale) : 44100.0
|
||||
let ptsValue = Double(ctx.appendCount) / sampleRate
|
||||
let pts = CMTime(seconds: ptsValue, preferredTimescale: Int32(sampleRate))
|
||||
|
||||
// 创建 CMSampleBuffer(使用 timeRange 的 start 作为 PTS)
|
||||
var sampleBuffer: CMSampleBuffer?
|
||||
let createStatus = CMAudioSampleBufferCreateReadyWithPacketDescriptions(
|
||||
allocator: kCFAllocatorDefault,
|
||||
dataBuffer: bb,
|
||||
formatDescription: fmtDesc,
|
||||
formatDescription: fd,
|
||||
sampleCount: actualFrames,
|
||||
presentationTimeStamp: timeRange.start,
|
||||
presentationTimeStamp: pts,
|
||||
packetDescriptions: nil,
|
||||
sampleBufferOut: &sampleBuffer
|
||||
)
|
||||
|
||||
guard createStatus == noErr, let sb = sampleBuffer else { return }
|
||||
|
||||
if writerInput.append(sb) {
|
||||
ctx.appendCount += actualFrames
|
||||
if !ctx.firstAppendLogged {
|
||||
ctx.firstAppendLogged = true
|
||||
print("[Recorder] ✓ First audio frame appended! pts=\(pts.seconds)s, frames=\(actualFrames)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 视频源录制器:AVPlayerItemVideoOutput 抓帧 + MTAudioProcessingTap 捕获音频
|
||||
// 空的 init/finalize 回调
|
||||
private func tapInit(_ tap: MTAudioProcessingTap) {}
|
||||
private func tapFinalize(_ tap: MTAudioProcessingTap) {}
|
||||
|
||||
/// 视频源录制器:从 AVPlayerItemVideoOutput 抓帧 + MTAudioProcessingTap 捕获音频,写入 mp4
|
||||
@MainActor
|
||||
final class PlayerRecorder: NSObject {
|
||||
|
||||
@ -157,12 +146,13 @@ final class PlayerRecorder: NSObject {
|
||||
private var weakOutput: AVPlayerItemVideoOutput?
|
||||
nonisolated(unsafe) private var isRunning = false
|
||||
|
||||
// 视频:缓存上一帧
|
||||
// 视频:缓存上一帧,确保静态场景也能持续写入
|
||||
private var lastPixelBuffer: CVPixelBuffer?
|
||||
private var captureFrameCount: Int = 0
|
||||
|
||||
// 音频 (MTAudioProcessingTap)
|
||||
private var audioTap: MTAudioProcessingTap?
|
||||
private var audioContext: AudioCaptureContext?
|
||||
private weak var currentPlayerItem: AVPlayerItem?
|
||||
|
||||
nonisolated(unsafe) private var recordStartTime: CMTime = .zero
|
||||
@ -236,14 +226,15 @@ final class PlayerRecorder: NSObject {
|
||||
audioInput = aInput
|
||||
}
|
||||
|
||||
guard writer.startWriting() else {
|
||||
let success = writer.startWriting()
|
||||
if !success {
|
||||
print("[Recorder] startWriting failed: \(writer.error?.localizedDescription ?? "unknown")")
|
||||
onError?("Cannot start writer: \(writer.error?.localizedDescription ?? "unknown")")
|
||||
return
|
||||
}
|
||||
|
||||
writer.startSession(atSourceTime: .zero)
|
||||
print("[Recorder] Writer started")
|
||||
print("[Recorder] Writer started, session at .zero")
|
||||
|
||||
// 启动视频抓帧
|
||||
isRecording = true
|
||||
@ -251,7 +242,7 @@ final class PlayerRecorder: NSObject {
|
||||
startTimer()
|
||||
startCaptureLoop()
|
||||
|
||||
// 启动音频捕获
|
||||
// 启动音频捕获(MTAudioProcessingTap)
|
||||
setupAudioTap(playerItem: playerItem, audioWriterInput: aInput)
|
||||
}
|
||||
|
||||
@ -265,9 +256,16 @@ final class PlayerRecorder: NSObject {
|
||||
stopTimer()
|
||||
|
||||
// 停止音频 tap
|
||||
audioContext?.isRunning = false
|
||||
|
||||
// 移除 audioMix(移除 tap)
|
||||
currentPlayerItem?.audioMix = nil
|
||||
currentPlayerItem = nil
|
||||
|
||||
// 释放 tap 和 context
|
||||
audioTap = nil
|
||||
audioContext = nil
|
||||
gAudioContext = nil
|
||||
|
||||
videoInput?.markAsFinished()
|
||||
audioInput?.markAsFinished()
|
||||
@ -275,8 +273,8 @@ final class PlayerRecorder: NSObject {
|
||||
writer?.finishWriting { [weak self] in
|
||||
Task { @MainActor in
|
||||
guard let self = self, let w = self.writer else { return }
|
||||
let vFrames = self.captureFrameCount
|
||||
print("[Recorder] finishWriting status: \(w.status.rawValue), error: \(w.error?.localizedDescription ?? "none"), video frames: \(vFrames)")
|
||||
let count = self.captureFrameCount
|
||||
print("[Recorder] finishWriting status: \(w.status.rawValue), error: \(w.error?.localizedDescription ?? "none"), video frames: \(count)")
|
||||
if w.status == .completed, let url = self.tempURL {
|
||||
self.showSaveDialog(tempURL: url)
|
||||
} else {
|
||||
@ -299,22 +297,29 @@ final class PlayerRecorder: NSObject {
|
||||
do {
|
||||
let audioTracks = try await asset.loadTracks(withMediaType: .audio)
|
||||
guard let audioTrack = audioTracks.first else {
|
||||
print("[Recorder] No audio track for tap")
|
||||
print("[Recorder] ❌ No audio track for tap")
|
||||
return
|
||||
}
|
||||
guard self.isRunning else {
|
||||
print("[Recorder] ❌ isRunning=false, aborting audio tap setup")
|
||||
return
|
||||
}
|
||||
guard self.isRunning else { return }
|
||||
|
||||
// 创建 context,用 Unmanaged 传递给 C 回调
|
||||
print("[Recorder] Audio track found: \(audioTrack.trackID), setting up tap...")
|
||||
|
||||
// 创建音频捕获上下文
|
||||
let context = AudioCaptureContext()
|
||||
context.writerInput = audioWriterInput
|
||||
context.isRunning = true
|
||||
self.audioContext = context
|
||||
|
||||
let contextPtr = Unmanaged.passRetained(context).toOpaque()
|
||||
// ★ 设置全局引用,供 C 回调访问
|
||||
gAudioContext = context
|
||||
|
||||
// 创建回调结构体
|
||||
// 创建 MTAudioProcessingTap
|
||||
var callbacks = MTAudioProcessingTapCallbacks(
|
||||
version: kMTAudioProcessingTapCallbacksVersion_0,
|
||||
clientInfo: contextPtr,
|
||||
clientInfo: nil,
|
||||
init: tapInit,
|
||||
finalize: tapFinalize,
|
||||
prepare: tapPrepare,
|
||||
@ -323,33 +328,31 @@ final class PlayerRecorder: NSObject {
|
||||
)
|
||||
|
||||
var tap: MTAudioProcessingTap?
|
||||
let status = withUnsafePointer(to: &callbacks) { cbPtr in
|
||||
MTAudioProcessingTapCreate(
|
||||
kCFAllocatorDefault,
|
||||
cbPtr,
|
||||
kMTAudioProcessingTapCreationFlag_PostEffects,
|
||||
&tap
|
||||
)
|
||||
}
|
||||
let status = MTAudioProcessingTapCreate(
|
||||
kCFAllocatorDefault,
|
||||
&callbacks,
|
||||
kMTAudioProcessingTapCreationFlag_PostEffects,
|
||||
&tap
|
||||
)
|
||||
|
||||
guard status == noErr, let audioTap = tap else {
|
||||
print("[Recorder] MTAudioProcessingTapCreate failed: \(status)")
|
||||
Unmanaged<AudioCaptureContext>.fromOpaque(contextPtr).release()
|
||||
print("[Recorder] ❌ MTAudioProcessingTapCreate failed: \(status)")
|
||||
gAudioContext = nil
|
||||
return
|
||||
}
|
||||
|
||||
self.audioTap = audioTap
|
||||
|
||||
// 创建 audioMix
|
||||
// 创建 audioMix 并挂上 tap
|
||||
let params = AVMutableAudioMixInputParameters(track: audioTrack)
|
||||
params.audioTapProcessor = audioTap
|
||||
let audioMix = AVMutableAudioMix()
|
||||
audioMix.inputParameters = [params]
|
||||
|
||||
playerItem.audioMix = audioMix
|
||||
print("[Recorder] Audio tap installed")
|
||||
print("[Recorder] ✓ Audio tap installed on player item")
|
||||
} catch {
|
||||
print("[Recorder] Audio tap setup failed: \(error)")
|
||||
print("[Recorder] ❌ Audio tap setup failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -372,12 +375,14 @@ final class PlayerRecorder: NSObject {
|
||||
let hostTime = CACurrentMediaTime()
|
||||
let itemTime = output.itemTime(forHostTime: hostTime)
|
||||
|
||||
// 尝试获取新帧,否则复用上一帧
|
||||
if let pb = output.copyPixelBuffer(forItemTime: itemTime, itemTimeForDisplay: nil) {
|
||||
lastPixelBuffer = pb
|
||||
}
|
||||
|
||||
guard let pb = lastPixelBuffer else { return }
|
||||
|
||||
// 相对时间戳
|
||||
let relativeTime = CMTimeSubtract(itemTime, recordStartTime)
|
||||
guard relativeTime.seconds >= 0 else { return }
|
||||
|
||||
@ -405,7 +410,7 @@ final class PlayerRecorder: NSObject {
|
||||
return sampleBuffer
|
||||
}
|
||||
|
||||
// MARK: - Timer
|
||||
// MARK: - Timer (显示录制时长)
|
||||
private func startTimer() {
|
||||
startDate = Date()
|
||||
durationText = "00:00"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user