Recording: red icon, live duration display, save dialog with editable filename

This commit is contained in:
yumoqing 2026-06-24 19:56:07 +08:00
parent 35de7263e9
commit 87c66c3c70
3 changed files with 80 additions and 10 deletions

View File

@ -325,8 +325,21 @@ struct ControlToolbar: View {
//
HStack(spacing: 10) {
//
TB(icon: bridge.screenRecorder.isRecording ? "stop.circle.fill" : "record.circle") {
bridge.toggleRecording(); onTouch()
if bridge.screenRecorder.isRecording {
HStack(spacing: 4) {
Image(systemName: "stop.circle.fill")
.foregroundColor(.red)
.font(.system(size: 18))
Text(bridge.screenRecorder.durationText)
.font(.system(size: 12, design: .monospaced))
.foregroundColor(.red)
}
.contentShape(Rectangle())
.onTapGesture { bridge.toggleRecording(); onTouch() }
} else {
TB(icon: "record.circle") {
bridge.toggleRecording(); onTouch()
}
}
Divider().frame(height: 20).padding(.horizontal, 4)

View File

@ -100,7 +100,7 @@ final class PlayerBridge: ObservableObject {
setupEndObserver()
#if os(macOS)
screenRecorder.onRecordingStopped = { [weak self] url in
screenRecorder.onRecordingSaved = { [weak self] url in
self?.showToast("Saved: \(url.lastPathComponent)")
}
screenRecorder.onError = { [weak self] msg in

View File

@ -9,9 +9,13 @@ final class ScreenRecorder: NSObject, AVCaptureFileOutputRecordingDelegate {
private var session: AVCaptureSession?
private var fileOutput: AVCaptureMovieFileOutput?
private var isConfigured = false
private var timer: Timer?
private var startDate: Date?
@Published var isRecording = false
var onRecordingStopped: ((URL) -> Void)?
@Published var durationText = "00:00"
var onRecordingSaved: ((URL) -> Void)?
var onError: ((String) -> Void)?
///
@ -51,12 +55,66 @@ final class ScreenRecorder: NSObject, AVCaptureFileOutputRecordingDelegate {
fileOutput.startRecording(to: outputURL, recordingDelegate: self)
}
///
///
func stopRecording() {
guard isRecording, let fileOutput = fileOutput else { return }
fileOutput.stopRecording()
}
private func startTimer() {
startDate = Date()
durationText = "00:00"
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
Task { @MainActor in
guard let self = self, let start = self.startDate else { return }
let elapsed = Int(Date().timeIntervalSince(start))
let m = elapsed / 60
let s = elapsed % 60
let h = m / 60
if h > 0 {
self.durationText = String(format: "%d:%02d:%02d", h, m % 60, s)
} else {
self.durationText = String(format: "%02d:%02d", m, s)
}
}
}
}
private func stopTimer() {
timer?.invalidate()
timer = nil
startDate = nil
durationText = "00:00"
}
///
private func showSaveDialog(tempURL: URL) {
let panel = NSSavePanel()
panel.nameFieldStringValue = tempURL.lastPathComponent
panel.allowedContentTypes = [.mpeg4Movie]
panel.directoryURL = saveDirectory
panel.title = "Save Recording"
panel.begin { [weak self] response in
guard let self = self else { return }
if response == .OK, let destURL = panel.url {
do {
//
if FileManager.default.fileExists(atPath: destURL.path) {
try FileManager.default.removeItem(at: destURL)
}
try FileManager.default.moveItem(at: tempURL, to: destURL)
self.onRecordingSaved?(destURL)
} catch {
self.onError?("Save failed: \(error.localizedDescription)")
}
} else {
//
try? FileManager.default.removeItem(at: tempURL)
}
}
}
private func configureSession() -> Bool {
let session = AVCaptureSession()
session.sessionPreset = .hd1920x1080
@ -89,7 +147,6 @@ final class ScreenRecorder: NSObject, AVCaptureFileOutputRecordingDelegate {
session.addInput(audioInput)
}
}
//
//
let output = AVCaptureMovieFileOutput()
@ -103,8 +160,7 @@ final class ScreenRecorder: NSObject, AVCaptureFileOutputRecordingDelegate {
self.session = session
self.fileOutput = output
// macOS 10.15+
// CGRequestScreenCaptureAccess
//
#if canImport(ScreenCaptureKit)
if !CGPreflightScreenCaptureAccess() {
CGRequestScreenCaptureAccess()
@ -118,18 +174,19 @@ final class ScreenRecorder: NSObject, AVCaptureFileOutputRecordingDelegate {
nonisolated func fileOutputRecordingDidStart(_ output: AVCaptureFileOutput) {
Task { @MainActor in
self.isRecording = true
self.startTimer()
}
}
nonisolated func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
Task { @MainActor in
self.isRecording = false
self.stopTimer()
if let error = error {
self.onError?("Recording failed: \(error.localizedDescription)")
} else {
self.onRecordingStopped?(outputFileURL)
self.showSaveDialog(tempURL: outputFileURL)
}
// session
self.session?.stopRunning()
}
}