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:
yumoqing 2026-06-25 09:08:02 +08:00
parent 6e94bb46b7
commit be0cf09941
2 changed files with 108 additions and 68 deletions

View File

@ -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
}
}
}

View File

@ -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 tracktrackID audioMix
// 2) asset.loadTracksfallbackHLS 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")
// 2fallback 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() {