feat: 录制完成后弹出保存对话框,支持用户改名
- 添加 showSaveDialog 方法,使用 NSSavePanel - 默认文件名为 Recording_时间戳.mp4 - 用户可修改文件名或选择保存位置 - 点击取消(X)时删除临时文件 - 保存成功后调用 onRecordingSaved 回调
This commit is contained in:
parent
ff20ffe07f
commit
fc95254500
@ -305,16 +305,8 @@ final class PlayerRecorder: NSObject {
|
||||
vFrames, totalAudioFrames)
|
||||
|
||||
if w.status == .completed, let tempURL = self.tempURL {
|
||||
// 边录边写完成,仅做改名
|
||||
let finalFilename = self.generateFinalFilename()
|
||||
let finalURL = self.saveDirectory.appendingPathComponent(finalFilename)
|
||||
do {
|
||||
try FileManager.default.moveItem(at: tempURL, to: finalURL)
|
||||
NSLog("[Recorder] ✓ Saved: %@", finalURL.path)
|
||||
self.onRecordingSaved?(finalURL)
|
||||
} catch {
|
||||
self.onError?("Save failed: \(error.localizedDescription)")
|
||||
}
|
||||
// 弹出保存对话框,用户可改名
|
||||
self.showSaveDialog(tempURL: tempURL)
|
||||
} else {
|
||||
self.onError?("Recording failed: \(w.error?.localizedDescription ?? "unknown (status=\(w.status.rawValue))")")
|
||||
if let url = self.tempURL {
|
||||
@ -330,69 +322,71 @@ final class PlayerRecorder: NSObject {
|
||||
// MARK: - 音频 Tap 设置
|
||||
|
||||
private func setupAudioTap(playerItem: AVPlayerItem, audioWriterInput: AVAssetWriterInput) {
|
||||
let asset = playerItem.asset
|
||||
// 使用 playerItem.tracks(动态加载)而不是 asset.loadTracks
|
||||
// HLS 流的 tracks 需要等待加载完成后才可用
|
||||
|
||||
Task {
|
||||
do {
|
||||
let audioTracks = try await asset.loadTracks(withMediaType: .audio)
|
||||
guard let audioTrack = audioTracks.first else {
|
||||
NSLog("[Recorder] ❌ No audio track for tap")
|
||||
return
|
||||
}
|
||||
guard self.isRunning else {
|
||||
NSLog("[Recorder] ❌ isRunning=false, aborting audio tap setup")
|
||||
// 等待 tracks 加载
|
||||
var attempts = 0
|
||||
while attempts < 50 { // 最多等待 5 秒
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
} catch {
|
||||
NSLog("[Recorder] Audio tap setup error: %@", error.localizedDescription)
|
||||
attempts += 1
|
||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒
|
||||
}
|
||||
|
||||
NSLog("[Recorder] ❌ Timeout waiting for audio track")
|
||||
}
|
||||
}
|
||||
|
||||
@ -494,6 +488,43 @@ final class PlayerRecorder: NSObject {
|
||||
formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
|
||||
return "Recording_\(formatter.string(from: Date())).mp4"
|
||||
}
|
||||
|
||||
// MARK: - 保存对话框
|
||||
|
||||
private func showSaveDialog(tempURL: URL) {
|
||||
let panel = NSSavePanel()
|
||||
panel.nameFieldStringValue = generateFinalFilename()
|
||||
panel.allowedContentTypes = [.mpeg4Movie]
|
||||
panel.directoryURL = saveDirectory
|
||||
panel.title = "保存录制文件"
|
||||
panel.prompt = "保存"
|
||||
|
||||
panel.begin { [weak self] response in
|
||||
Task { @MainActor in
|
||||
guard let self = self else { return }
|
||||
|
||||
if response == .OK, let finalURL = panel.url {
|
||||
do {
|
||||
// 如果目标文件已存在,先删除
|
||||
if FileManager.default.fileExists(atPath: finalURL.path) {
|
||||
try FileManager.default.removeItem(at: finalURL)
|
||||
}
|
||||
// 移动临时文件到最终位置
|
||||
try FileManager.default.moveItem(at: tempURL, to: finalURL)
|
||||
NSLog("[Recorder] ✓ Saved: %@", finalURL.path)
|
||||
self.onRecordingSaved?(finalURL)
|
||||
} catch {
|
||||
self.onError?("Save failed: \(error.localizedDescription)")
|
||||
try? FileManager.default.removeItem(at: tempURL)
|
||||
}
|
||||
} else {
|
||||
// 用户取消,删除临时文件
|
||||
NSLog("[Recorder] User cancelled save, deleting temp file")
|
||||
try? FileManager.default.removeItem(at: tempURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user