diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift index 4011763c5..ad242a07c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift @@ -33,6 +33,19 @@ public struct MiniPlayer: View { audioController.item != nil && audioController.state != .stopped } + var playPauseButtonImage: String { + switch audioController.state { + case .playing: + return "pause.circle" + case .paused: + return "play.circle" + case .reachedEnd: + return "gobackward" + default: + return "" + } + } + var playPauseButtonItem: some View { if let item = audioController.item, audioController.isLoadingItem(item: item) { return AnyView(ProgressView()) @@ -44,12 +57,15 @@ public struct MiniPlayer: View { _ = audioController.pause() case .paused: _ = audioController.unpause() + case .reachedEnd: + audioController.seek(to: 0.0) + audioController.unpause() default: break } }, label: { - Image(systemName: audioController.state == .playing ? "pause.circle" : "play.circle") + Image(systemName: playPauseButtonImage) .font(expanded ? .system(size: 64.0, weight: .thin) : .appTitleTwo) } )) diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift index efb755440..508e923b5 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift @@ -18,6 +18,7 @@ public enum AudioControllerState { case paused case loading case playing + case reachedEnd } public enum PlayerScrubState { @@ -53,12 +54,14 @@ let VOICES = [ class SpeechPlayerItem: AVPlayerItem { let session: AudioController let speechItem: SpeechItem + let completed: () -> Void var observer: Any? - init(session: AudioController, speechItem: SpeechItem, url: URL) { + init(session: AudioController, speechItem: SpeechItem, url: URL, completed: @escaping () -> Void) { self.session = session self.speechItem = speechItem + self.completed = completed let asset = AVAsset(url: url) super.init(asset: asset, automaticallyLoadedAssetKeys: nil) @@ -67,6 +70,10 @@ class SpeechPlayerItem: AVPlayerItem { self.observer = observe(\.status, options: [.new]) { item, _ in item.session.updateDuration(forItem: item.speechItem, newDuration: CMTimeGetSeconds(item.duration)) } + + NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self, queue: OperationQueue.main) { _ in + self.completed() + } } } @@ -265,7 +272,10 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate } public func isLoadingItem(item: LinkedItem) -> Bool { - (state == .loading || player?.currentItem == nil || player?.currentItem?.status == .unknown) && self.item == item + if state == .reachedEnd { + return false + } + return self.item == item && (state == .loading || player?.currentItem == nil || player?.currentItem?.status == .unknown) } public func isPlayingItem(item: LinkedItem) -> Bool { @@ -332,7 +342,14 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate if let synthesizer = synthesizer { for await speechItem in synthesizer.fetch(from: start) { DispatchQueue.main.async { - let item = SpeechPlayerItem(session: self, speechItem: speechItem, url: speechItem.audioURL) + let isLast = speechItem.audioIdx == synthesizer.document.utterances.count - 1 + let item = SpeechPlayerItem(session: self, speechItem: speechItem, url: speechItem.audioURL) { + // Pause player when we complete the final item. + if isLast { + self.player?.pause() + self.state = .reachedEnd + } + } self.player?.insert(item, after: nil) if playWhenReady, self.player?.items().count == 1 { @@ -400,7 +417,7 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate if player.items().count == 1, let currentTime = player.currentItem?.currentTime(), let duration = player.currentItem?.duration { if currentTime >= duration { pause() - seek(to: 0) + state = .reachedEnd } }