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)
|
vFrames, totalAudioFrames)
|
||||||
|
|
||||||
if w.status == .completed, let tempURL = self.tempURL {
|
if w.status == .completed, let tempURL = self.tempURL {
|
||||||
// 边录边写完成,仅做改名
|
// 弹出保存对话框,用户可改名
|
||||||
let finalFilename = self.generateFinalFilename()
|
self.showSaveDialog(tempURL: tempURL)
|
||||||
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)")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
self.onError?("Recording failed: \(w.error?.localizedDescription ?? "unknown (status=\(w.status.rawValue))")")
|
self.onError?("Recording failed: \(w.error?.localizedDescription ?? "unknown (status=\(w.status.rawValue))")")
|
||||||
if let url = self.tempURL {
|
if let url = self.tempURL {
|
||||||
@ -330,21 +322,18 @@ final class PlayerRecorder: NSObject {
|
|||||||
// MARK: - 音频 Tap 设置
|
// MARK: - 音频 Tap 设置
|
||||||
|
|
||||||
private func setupAudioTap(playerItem: AVPlayerItem, audioWriterInput: AVAssetWriterInput) {
|
private func setupAudioTap(playerItem: AVPlayerItem, audioWriterInput: AVAssetWriterInput) {
|
||||||
let asset = playerItem.asset
|
// 使用 playerItem.tracks(动态加载)而不是 asset.loadTracks
|
||||||
|
// HLS 流的 tracks 需要等待加载完成后才可用
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
// 等待 tracks 加载
|
||||||
let audioTracks = try await asset.loadTracks(withMediaType: .audio)
|
var attempts = 0
|
||||||
guard let audioTrack = audioTracks.first else {
|
while attempts < 50 { // 最多等待 5 秒
|
||||||
NSLog("[Recorder] ❌ No audio track for tap")
|
guard self.isRunning else { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
guard self.isRunning else {
|
|
||||||
NSLog("[Recorder] ❌ isRunning=false, aborting audio tap setup")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
NSLog("[Recorder] Audio track found: %d, setting up tap...", audioTrack.trackID)
|
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()
|
let context = AudioCaptureContext()
|
||||||
@ -390,9 +379,14 @@ final class PlayerRecorder: NSObject {
|
|||||||
|
|
||||||
playerItem.audioMix = audioMix
|
playerItem.audioMix = audioMix
|
||||||
NSLog("[Recorder] ✓ Audio tap installed on playerItem")
|
NSLog("[Recorder] ✓ Audio tap installed on playerItem")
|
||||||
} catch {
|
return
|
||||||
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"
|
formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
|
||||||
return "Recording_\(formatter.string(from: Date())).mp4"
|
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
|
#endif
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user