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