Archived
feat: hardening, error states, interruption handling, a11y (Phase 8)
This commit is contained in:
1 parent
9f1f26ba54
commit
12ded179eb
6 files changed
+233
-38
No files matched your search
@@ -16,6 +16,7 @@ final class MediaPlayerController: ObservableObject {
|
||||
@Published private(set) var duration: Double = 0
|
||||
@Published private(set) var pendingJobs: [MediaPendingJob] = []
|
||||
@Published private(set) var choices: [MediaChoice] = []
|
||||
@Published private(set) var stateError: String?
|
||||
|
||||
let player = AVPlayer()
|
||||
|
||||
@@ -34,6 +35,11 @@ final class MediaPlayerController: ObservableObject {
|
||||
private var timeObserver: Any?
|
||||
private var statusCancellable: AnyCancellable?
|
||||
|
||||
/// Bumped on every room-scope change so in-flight /state polls from the
|
||||
/// previous scope cannot reconcile into the new one (port of the web UI's
|
||||
/// `=== room` checks).
|
||||
private var pollGeneration = 0
|
||||
|
||||
init(api: MediaAPI, settings: SettingsStore) {
|
||||
self.api = api
|
||||
self.settings = settings
|
||||
@@ -52,6 +58,25 @@ final class MediaPlayerController: ObservableObject {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
@objc private func handleInterruption(_ notification: Notification) {
|
||||
guard let info = notification.userInfo else { return }
|
||||
guard let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
||||
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
|
||||
switch type {
|
||||
case .began:
|
||||
player.pause()
|
||||
logger.log("audio interruption began, paused media")
|
||||
case .ended:
|
||||
applyVolume()
|
||||
if currentTrackID != nil, desired == "playing", loadedAudioPath != nil {
|
||||
player.play()
|
||||
logger.log("audio interruption ended, resumed media")
|
||||
}
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
func attach(session: VoiceSession) {
|
||||
@@ -63,6 +88,7 @@ final class MediaPlayerController: ObservableObject {
|
||||
/// Called just before joining the LiveKit room. Carries whatever is loaded
|
||||
/// into the new room-scoped player state, paused (talking pauses music).
|
||||
func enterMediaScope(roomName: String) async {
|
||||
pollGeneration += 1
|
||||
let wasPlaying = currentTrackID != nil && !player.rate.isZero
|
||||
wasPlayingBeforeTalk = wasPlaying
|
||||
let carriedTrack = currentTrackID
|
||||
@@ -83,6 +109,7 @@ final class MediaPlayerController: ObservableObject {
|
||||
/// hand the room back to the player.
|
||||
func endConversation() async {
|
||||
stopPolling()
|
||||
pollGeneration += 1
|
||||
if wasPlayingBeforeTalk {
|
||||
wasPlayingBeforeTalk = false
|
||||
guard let room else { return }
|
||||
@@ -120,12 +147,17 @@ final class MediaPlayerController: ObservableObject {
|
||||
// MARK: Poll + reconcile (ported from app.js pollMediaState/reconcileMedia)
|
||||
|
||||
private func pollState() async {
|
||||
let generation = pollGeneration
|
||||
let scope = room ?? "default"
|
||||
do {
|
||||
let state = try await api.state(room: scope)
|
||||
guard generation == pollGeneration else { return }
|
||||
stateError = nil
|
||||
reconcile(state)
|
||||
} catch {
|
||||
guard generation == pollGeneration else { return }
|
||||
logger.log("poll failed: \(String(describing: error), privacy: .public)")
|
||||
stateError = "Media server unreachable: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,10 +278,6 @@ final class MediaPlayerController: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleInterruption(_ notification: Notification) {
|
||||
applyVolume()
|
||||
}
|
||||
|
||||
// MARK: Reporting + publishing
|
||||
|
||||
private func report(_ state: String) {
|
||||
@@ -344,6 +372,7 @@ final class MediaPlayerController: ObservableObject {
|
||||
player.replaceCurrentItem(with: nil)
|
||||
currentTime = 0
|
||||
duration = 0
|
||||
stateError = nil
|
||||
}
|
||||
|
||||
// MARK: Volume
|
||||
|
||||
Reference in new issue
Block a user