Files

257 lines
8.3 KiB
Swift

//
// RecordingViewModel.swift
// Lecture2Quiz
//
// Created by ParkMazorika on 4/27/25.
//
import Foundation
/// WebSocket client that connects to a transcription server and handles streaming, JSON messages, and retries.
class AudioWebSocket: NSObject, URLSessionWebSocketDelegate {
private var webSocketTask: URLSessionWebSocketTask?
private var urlSession: URLSession!
private let host: String
private let port: Int
private var retryCount = 0
private let maxRetries = 3
private var uid: String
private let modelSize: String
private var pingTimer: Timer?
private var processedTexts = Set<String>()
var onServerReady: (() -> Void)?
var onTranscriptionReceived: ((String) -> Void)?
init(host: String, port: Int, modelSize: String = "medium") {
self.host = host
self.port = port
self.uid = UUID().uuidString
self.modelSize = modelSize
super.init()
self.urlSession = URLSession(
configuration: .default,
delegate: self,
delegateQueue: .main
)
connect()
}
/// Establishes a WebSocket connection with the configured server.
private func connect() {
guard retryCount <= maxRetries else {
print("Maximum reconnect attempts exceeded.")
return
}
let socketURL = port == 443 || port == 80
? "wss://\(host)"
: "wss://\(host):\(port)"
guard let url = URL(string: socketURL) else {
print("Invalid URL: \(socketURL)")
return
}
webSocketTask = urlSession.webSocketTask(with: url)
webSocketTask?.resume()
print("Attempting WebSocket connection: \(socketURL)")
listen()
sendInitialJSON()
startPing()
}
/// Sends the initial JSON payload to identify and configure the session.
private func sendInitialJSON() {
let jsonPayload: [String: Any] = [
"uid": uid,
"language": "en",
"task": "transcribe",
"model": modelSize,
"use_vad": true,
"max_clients": 4,
"max_connection_time": 600
]
do {
let jsonData = try JSONSerialization.data(withJSONObject: jsonPayload, options: [])
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
print("Sending config JSON: \(jsonString)")
webSocketTask?.send(.string(jsonString)) { [weak self] error in
if let error = error {
print("Failed to send config JSON: \(error.localizedDescription)")
self?.reconnect()
} else {
print("Config JSON sent successfully.")
}
}
} catch {
print("JSON serialization error: \(error.localizedDescription)")
}
}
/// Sends audio data to the server.
func sendDataToServer(_ data: Data) {
guard isConnected else {
print("Not connected - skipping data send.")
reconnect()
return
}
webSocketTask?.send(.data(data)) { [weak self] error in
if let error = error {
print("Failed to send audio data: \(error.localizedDescription)")
self?.reconnect()
} else {
print("Sent audio data: \(data.count) bytes")
}
}
}
/// Returns true if the WebSocket is currently connected.
internal var isConnected: Bool {
webSocketTask?.state == .running
}
/// Attempts reconnection with exponential backoff.
private func reconnect() {
retryCount += 1
stopPing()
let delay = min(5.0, pow(2.0, Double(retryCount)))
DispatchQueue.global().asyncAfter(deadline: .now() + delay) { [weak self] in
print("Reconnecting... (\(self?.retryCount ?? 0)/\(self?.maxRetries ?? 0))")
self?.connect()
}
}
/// Starts listening for incoming messages from the server.
private func listen() {
webSocketTask?.receive { [weak self] result in
switch result {
case .success(let message):
self?.handleMessage(message)
self?.listen()
case .failure(let error):
print("Receive error: \(error.localizedDescription)")
self?.reconnect()
}
}
}
/// Handles incoming WebSocket messages (text or binary).
private func handleMessage(_ message: URLSessionWebSocketTask.Message) {
switch message {
case .data(let data):
print("Received binary data: \(data.count) bytes")
case .string(let text):
print("Received text message: \(text)")
guard let data = text.data(using: .utf8) else { return }
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
if let status = json["status"] as? String {
handleStatusMessage(status: status, message: json["message"] as? String)
return
}
if let message = json["message"] as? String, message == "SERVER_READY" {
print("Server is ready.")
onServerReady?()
return
}
if let segments = json["segments"] as? [[String: Any]] {
let wrapped = ["segments": segments]
let segmentData = try JSONSerialization.data(withJSONObject: wrapped, options: [])
let segmentString = String(data: segmentData, encoding: .utf8)!
onTranscriptionReceived?(segmentString)
print("Transcription segments forwarded.")
}
}
} catch {
print("JSON parsing error: \(error.localizedDescription)")
}
@unknown default:
print("Unknown message type received.")
}
}
/// Handles status message JSON from the server.
private func handleStatusMessage(status: String, message: String?) {
switch status {
case "WAIT":
print("Waiting: \(message ?? "")")
case "ERROR":
print("Error: \(message ?? "")")
case "WARNING":
print("Warning: \(message ?? "")")
default:
print("\(status): \(message ?? "")")
}
}
/// Sends the "END_OF_AUDIO" signal to the server.
func sendEndOfAudio() {
guard isConnected else {
print("Not connected - skipping END_OF_AUDIO.")
return
}
webSocketTask?.send(.string("END_OF_AUDIO")) { error in
if let error = error {
print("Failed to send END_OF_AUDIO: \(error.localizedDescription)")
} else {
print("END_OF_AUDIO sent.")
}
}
}
/// Gracefully closes the WebSocket connection.
func closeConnection() {
stopPing()
webSocketTask?.cancel(with: .normalClosure, reason: nil)
retryCount = maxRetries
print("WebSocket closed.")
}
/// Starts periodic ping to keep the WebSocket alive.
private func startPing() {
stopPing()
pingTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: true) { [weak self] _ in
self?.webSocketTask?.sendPing { error in
if let error = error {
print("Ping failed: \(error.localizedDescription)")
} else {
print("Ping sent successfully.")
}
}
}
RunLoop.main.add(pingTimer!, forMode: .common)
}
/// Stops the periodic ping timer.
private func stopPing() {
pingTimer?.invalidate()
pingTimer = nil
}
/// Called when the WebSocket is closed by the server.
func urlSession(_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
reason: Data?) {
let reasonString = String(data: reason ?? Data(), encoding: .utf8) ?? "No reason"
print("WebSocket closed - code: \(closeCode.rawValue), reason: \(reasonString)")
stopPing()
reconnect()
}
}