fix: HLS audio (playerItem.tracks + better tap callback) + window title sync with toolbar
- HLS录制音频: 改用playerItem.tracks查找trackID(fallback到asset.loadTracks) - tap回调: 始终先调GetSourceAudio保证管道畅通,加详细日志 - 窗口标题: onAppear初始隐藏,随toolbar一起30秒后消失/点击图标回来
This commit is contained in:
parent
6e94bb46b7
commit
be0cf09941
@ -265,8 +265,12 @@ struct PlayerContentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// 初始状态:标题随toolbar一起隐藏
|
||||
NSApplication.shared.windows.first?.titleVisibility = .hidden
|
||||
}
|
||||
.onChange(of: showToolbar) { _, newValue in
|
||||
// 同步控制窗口标题的显示/隐藏
|
||||
// 同步控制窗口标题的显示/隐藏(与toolbar联动)
|
||||
NSApplication.shared.windows.first?.titleVisibility = newValue ? .visible : .hidden
|
||||
if newValue {
|
||||
resetHideTimer()
|
||||
@ -277,10 +281,6 @@ struct PlayerContentView: View {
|
||||
.onDisappear {
|
||||
hideTimer?.invalidate()
|
||||
}
|
||||
.onAppear {
|
||||
// 初始状态:标题随toolbar一起隐藏
|
||||
NSApplication.shared.windows.first?.titleVisibility = .hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -64,12 +64,7 @@ private func tapProcessCallback(
|
||||
_ numberFramesOut: UnsafeMutablePointer<CMItemCount>,
|
||||
_ flagsOut: UnsafeMutablePointer<MTAudioProcessingTapFlags>
|
||||
) {
|
||||
numberFramesOut.pointee = 0
|
||||
flagsOut.pointee = 0
|
||||
|
||||
guard numberFrames > 0 else { return }
|
||||
|
||||
// 获取源音频数据 + 时间范围
|
||||
// 始终先获取源音频(保证音频管道正常运行)
|
||||
var timeRange = CMTimeRange()
|
||||
var srcFlags: MTAudioProcessingTapFlags = 0
|
||||
var actualFrames: CMItemCount = 0
|
||||
@ -79,18 +74,25 @@ private func tapProcessCallback(
|
||||
&srcFlags, &timeRange, &actualFrames
|
||||
)
|
||||
|
||||
// 必须始终设置输出帧数和标志(否则音频引擎可能阻塞)
|
||||
numberFramesOut.pointee = actualFrames
|
||||
flagsOut.pointee = srcFlags
|
||||
|
||||
guard status == noErr, actualFrames > 0 else { return }
|
||||
|
||||
// 通过全局变量获取 context
|
||||
// 通过全局变量获取 context — 未就绪时仅跳过录制,不影响播放
|
||||
guard let ctx = gAudioContext, ctx.isRunning,
|
||||
let writerInput = ctx.writerInput,
|
||||
writerInput.isReadyForMoreMediaData else { return }
|
||||
|
||||
ctx.processCallCount += 1
|
||||
|
||||
// 定期记录回调活跃状态(每500次调用打一次日志)
|
||||
if ctx.processCallCount == 1 || ctx.processCallCount % 500 == 0 {
|
||||
NSLog("[Recorder] tapProcess #%d: frames=%ld, timescale=%d",
|
||||
ctx.processCallCount, actualFrames, timeRange.duration.timescale)
|
||||
}
|
||||
|
||||
let bufferList = bufferListInOut.pointee
|
||||
guard bufferList.mNumberBuffers > 0 else { return }
|
||||
|
||||
@ -117,9 +119,14 @@ private func tapProcessCallback(
|
||||
guard replaceStatus == kCMBlockBufferNoErr else { return }
|
||||
|
||||
// 获取 format description
|
||||
guard let fd = ctx.formatDescription else { return }
|
||||
guard let fd = ctx.formatDescription else {
|
||||
if ctx.processCallCount == 1 {
|
||||
NSLog("[Recorder] ⚠️ formatDescription nil on first process call")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// PTS 基于累积帧数
|
||||
// PTS 基于累积帧数(与视频一样从0开始)
|
||||
let sampleRate = Double(timeRange.duration.timescale) > 0
|
||||
? Double(timeRange.duration.timescale) : 44100.0
|
||||
let ptsValue = Double(ctx.appendCount) / sampleRate
|
||||
@ -135,7 +142,12 @@ private func tapProcessCallback(
|
||||
packetDescriptions: nil,
|
||||
sampleBufferOut: &sampleBuffer
|
||||
)
|
||||
guard createStatus == noErr, let sb = sampleBuffer else { return }
|
||||
guard createStatus == noErr, let sb = sampleBuffer else {
|
||||
if ctx.processCallCount <= 3 {
|
||||
NSLog("[Recorder] ⚠️ CMAudioSampleBufferCreate failed: %d", createStatus)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if writerInput.append(sb) {
|
||||
ctx.appendCount += actualFrames
|
||||
@ -143,6 +155,8 @@ private func tapProcessCallback(
|
||||
ctx.firstAppendLogged = true
|
||||
NSLog("[Recorder] ✓ First audio appended: pts=%.2fs, frames=%ld", pts.seconds, actualFrames)
|
||||
}
|
||||
} else if ctx.processCallCount <= 3 {
|
||||
NSLog("[Recorder] ⚠️ writerInput.append failed (input ready=%d)", writerInput.isReadyForMoreMediaData)
|
||||
}
|
||||
}
|
||||
|
||||
@ -322,63 +336,23 @@ final class PlayerRecorder: NSObject {
|
||||
// MARK: - 音频 Tap 设置
|
||||
|
||||
private func setupAudioTap(playerItem: AVPlayerItem, audioWriterInput: AVAssetWriterInput) {
|
||||
// 使用 playerItem.tracks(动态加载)而不是 asset.loadTracks
|
||||
// HLS 流的 tracks 需要等待加载完成后才可用
|
||||
// 两步策略查找音频 track:
|
||||
// 1) playerItem.tracks(已加载的 track,trackID 与 audioMix 匹配)
|
||||
// 2) asset.loadTracks(fallback,HLS 可能 tracks 尚未填充)
|
||||
|
||||
Task {
|
||||
// 等待 tracks 加载
|
||||
var attempts = 0
|
||||
while attempts < 50 { // 最多等待 5 秒
|
||||
|
||||
// 阶段1:尝试 playerItem.tracks(最多 3 秒)
|
||||
while attempts < 30 {
|
||||
guard self.isRunning else { return }
|
||||
|
||||
let tracks = try await playerItem.asset.loadTracks(withMediaType: .audio)
|
||||
if let audioTrack = tracks.first {
|
||||
NSLog("[Recorder] ✓ Audio track found: %d, setting up tap...", audioTrack.trackID)
|
||||
|
||||
// 创建音频捕获上下文
|
||||
let context = AudioCaptureContext()
|
||||
context.writerInput = audioWriterInput
|
||||
context.isRunning = true
|
||||
self.audioContext = context
|
||||
|
||||
// 全局引用,供 C 回调访问
|
||||
gAudioContext = context
|
||||
|
||||
// 创建 MTAudioProcessingTap 回调结构体
|
||||
var callbacks = MTAudioProcessingTapCallbacks(
|
||||
version: kMTAudioProcessingTapCallbacksVersion_0,
|
||||
clientInfo: nil,
|
||||
init: tapInitCallback,
|
||||
finalize: tapFinalizeCallback,
|
||||
prepare: tapPrepareCallback,
|
||||
unprepare: tapUnprepareCallback,
|
||||
process: tapProcessCallback
|
||||
)
|
||||
|
||||
var tap: MTAudioProcessingTap?
|
||||
let status = MTAudioProcessingTapCreate(
|
||||
kCFAllocatorDefault,
|
||||
&callbacks,
|
||||
kMTAudioProcessingTapCreationFlag_PostEffects,
|
||||
&tap
|
||||
)
|
||||
|
||||
guard status == noErr, let audioTap = tap else {
|
||||
NSLog("[Recorder] ❌ MTAudioProcessingTapCreate failed: %d", status)
|
||||
gAudioContext = nil
|
||||
return
|
||||
}
|
||||
|
||||
self.audioTap = audioTap
|
||||
|
||||
// 创建 audioMix 并挂上 tap
|
||||
let params = AVMutableAudioMixInputParameters(track: audioTrack)
|
||||
params.audioTapProcessor = audioTap
|
||||
let audioMix = AVMutableAudioMix()
|
||||
audioMix.inputParameters = [params]
|
||||
|
||||
playerItem.audioMix = audioMix
|
||||
NSLog("[Recorder] ✓ Audio tap installed on playerItem")
|
||||
let itemTracks = playerItem.tracks
|
||||
if let audioItemTrack = itemTracks.first(where: { $0.assetTrack?.mediaType == .audio }),
|
||||
let assetTrack = audioItemTrack.assetTrack {
|
||||
let trackID = assetTrack.trackID
|
||||
NSLog("[Recorder] ✓ Audio track via playerItem.tracks: trackID=%d", trackID)
|
||||
self.installTap(on: playerItem, trackID: trackID, audioWriterInput: audioWriterInput)
|
||||
return
|
||||
}
|
||||
|
||||
@ -386,10 +360,76 @@ final class PlayerRecorder: NSObject {
|
||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒
|
||||
}
|
||||
|
||||
NSLog("[Recorder] ❌ Timeout waiting for audio track")
|
||||
// 阶段2:fallback — asset.loadTracks(最多再等 5 秒)
|
||||
NSLog("[Recorder] ⚠️ playerItem.tracks empty, falling back to asset.loadTracks")
|
||||
attempts = 0
|
||||
while attempts < 50 {
|
||||
guard self.isRunning else { return }
|
||||
|
||||
if let assetTracks = try? await playerItem.asset.loadTracks(withMediaType: .audio),
|
||||
let audioTrack = assetTracks.first {
|
||||
let trackID = audioTrack.trackID
|
||||
NSLog("[Recorder] ✓ Audio track via asset.loadTracks: trackID=%d", trackID)
|
||||
self.installTap(on: playerItem, trackID: trackID, audioWriterInput: audioWriterInput)
|
||||
return
|
||||
}
|
||||
|
||||
attempts += 1
|
||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒
|
||||
}
|
||||
|
||||
NSLog("[Recorder] ❌ No audio track found after both attempts")
|
||||
}
|
||||
}
|
||||
|
||||
private func installTap(on playerItem: AVPlayerItem, trackID: CMPersistentTrackID, audioWriterInput: AVAssetWriterInput) {
|
||||
// 创建音频捕获上下文
|
||||
let context = AudioCaptureContext()
|
||||
context.writerInput = audioWriterInput
|
||||
context.isRunning = true
|
||||
self.audioContext = context
|
||||
|
||||
// 全局引用,供 C 回调访问
|
||||
gAudioContext = context
|
||||
|
||||
// 创建 MTAudioProcessingTap 回调结构体
|
||||
var callbacks = MTAudioProcessingTapCallbacks(
|
||||
version: kMTAudioProcessingTapCallbacksVersion_0,
|
||||
clientInfo: nil,
|
||||
init: tapInitCallback,
|
||||
finalize: tapFinalizeCallback,
|
||||
prepare: tapPrepareCallback,
|
||||
unprepare: tapUnprepareCallback,
|
||||
process: tapProcessCallback
|
||||
)
|
||||
|
||||
var tap: MTAudioProcessingTap?
|
||||
let status = MTAudioProcessingTapCreate(
|
||||
kCFAllocatorDefault,
|
||||
&callbacks,
|
||||
kMTAudioProcessingTapCreationFlag_PostEffects,
|
||||
&tap
|
||||
)
|
||||
|
||||
guard status == noErr, let audioTap = tap else {
|
||||
NSLog("[Recorder] ❌ MTAudioProcessingTapCreate failed: %d", status)
|
||||
gAudioContext = nil
|
||||
return
|
||||
}
|
||||
|
||||
self.audioTap = audioTap
|
||||
|
||||
// 关键:用 playerItem 的 trackID 创建 audioMix input parameters
|
||||
let params = AVMutableAudioMixInputParameters()
|
||||
params.trackID = trackID
|
||||
params.audioTapProcessor = audioTap
|
||||
let audioMix = AVMutableAudioMix()
|
||||
audioMix.inputParameters = [params]
|
||||
|
||||
playerItem.audioMix = audioMix
|
||||
NSLog("[Recorder] ✓ Audio tap installed on playerItem (trackID=%d)", trackID)
|
||||
}
|
||||
|
||||
// MARK: - 视频抓帧 (30fps Timer)
|
||||
|
||||
private func startCaptureLoop() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user