Files
meetingnotes/meetingnotes/Managers/RecordingSessionManager.swift
T
coderandjames 20593bbc6e Add MuteDeck recording integration (#2)
* feat: add MuteDeck recording integration

* fix: make local API connection capture explicit

* ci: smoke test MuteDeck API listener

---------

Co-authored-by: superdooper86 <[email protected]>
2026-07-14 22:44:58 +02:00

153 lines
5.9 KiB
Swift

import Foundation
import SwiftUI
import Combine
/// Manages recording sessions at the app level to persist across navigation
@MainActor
class RecordingSessionManager: ObservableObject {
static let shared = RecordingSessionManager()
@Published var isRecording = false
@Published var isProcessing = false
@Published var activeMeetingId: UUID?
@Published var recordingStartedAt: Date?
@Published var errorMessage: String?
@Published var activeRecordingTranscriptChunksUpdated: [TranscriptChunk] = []
private let audioManager = AudioManager.shared
private var cancellables = Set<AnyCancellable>()
private let transcriptUpdateSubject = PassthroughSubject<[TranscriptChunk], Never>()
// Store transcript chunks for the active recording session
private var activeRecordingTranscriptChunks: [TranscriptChunk] = []
private init() {
setupAudioManagerBindings()
setupDebouncedSaving()
}
private func setupAudioManagerBindings() {
// Bind to audio manager state
audioManager.$isRecording
.sink { [weak self] isRecording in
self?.isRecording = isRecording
}
.store(in: &cancellables)
audioManager.$isProcessing
.sink { [weak self] isProcessing in
self?.isProcessing = isProcessing
}
.store(in: &cancellables)
audioManager.$errorMessage
.sink { [weak self] errorMessage in
self?.errorMessage = errorMessage
}
.store(in: &cancellables)
// When transcript chunks change, store them for the active recording and send to debouncer
audioManager.$transcriptChunks
.sink { [weak self] newChunks in
guard let self = self, self.isRecording, self.activeMeetingId != nil else { return }
self.activeRecordingTranscriptChunks = newChunks
self.activeRecordingTranscriptChunksUpdated = newChunks
self.transcriptUpdateSubject.send(newChunks)
}
.store(in: &cancellables)
}
private func setupDebouncedSaving() {
transcriptUpdateSubject
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { [weak self] chunks in
guard let self = self, let activeMeetingId = self.activeMeetingId else { return }
print("💾 Debounced save triggered for meeting: \(activeMeetingId.uuidString)")
self.updateActiveMeetingTranscript(meetingId: activeMeetingId, chunks: chunks)
}
.store(in: &cancellables)
}
func startRecording(for meetingId: UUID) {
print("🎙️ Starting recording for meeting: \(meetingId)")
// Load the meeting to get existing transcript chunks
if let existingMeeting = LocalStorageManager.shared.loadMeetings().first(where: { $0.id == meetingId }) {
activeRecordingTranscriptChunks = existingMeeting.transcriptChunks
// Seed the audio manager with existing chunks
audioManager.transcriptChunks = existingMeeting.transcriptChunks
}
activeMeetingId = meetingId
recordingStartedAt = Date()
audioManager.startRecording()
}
func stopRecording() async -> [TranscriptChunk] {
print("🛑 Stopping recording for meeting: \(activeMeetingId?.uuidString ?? "unknown")")
guard let meetingId = activeMeetingId else {
audioManager.cancelRecording()
recordingStartedAt = nil
return []
}
recordingStartedAt = nil
let chunks = await audioManager.stopRecordingAndTranscribe()
activeRecordingTranscriptChunks = chunks
activeRecordingTranscriptChunksUpdated = chunks
updateActiveMeetingTranscript(meetingId: meetingId, chunks: chunks)
activeMeetingId = nil
activeRecordingTranscriptChunks = []
return chunks
}
func cancelRecording() {
audioManager.cancelRecording()
activeMeetingId = nil
recordingStartedAt = nil
activeRecordingTranscriptChunks = []
}
func isRecordingMeeting(_ meetingId: UUID) -> Bool {
return isRecording && activeMeetingId == meetingId
}
private func updateActiveMeetingTranscript(meetingId: UUID, chunks: [TranscriptChunk]) {
// Load all meetings
var meetings = LocalStorageManager.shared.loadMeetings()
// Find and update the active meeting
if let index = meetings.firstIndex(where: { $0.id == meetingId }) {
meetings[index].transcriptChunks = chunks
// Save the updated meeting
let success = LocalStorageManager.shared.saveMeeting(meetings[index])
if success {
print("✅ Saved meeting transcript: \(meetingId.uuidString)")
NotificationCenter.default.post(name: .meetingSaved, object: meetings[index])
} else {
print("❌ Failed to save meeting transcript: \(meetingId.uuidString)")
}
}
}
func getActiveRecordingTranscriptChunks() -> [TranscriptChunk] {
return activeRecordingTranscriptChunks
}
/// Get transcript chunks for a specific meeting, ensuring proper data separation
func getTranscriptChunks(for meetingId: UUID) -> [TranscriptChunk] {
if isRecording && activeMeetingId == meetingId {
// Return live transcript chunks for the active recording
return activeRecordingTranscriptChunks
} else {
// Load saved transcript chunks from storage for non-active meetings
if let savedMeeting = LocalStorageManager.shared.loadMeetings().first(where: { $0.id == meetingId }) {
return savedMeeting.transcriptChunks
}
return []
}
}
}