mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
WIP: AudioSession and miniplayer for iOS TTS
This commit is contained in:
parent
ae4c727a64
commit
06036b7517
7 changed files with 184 additions and 24 deletions
|
|
@ -12,11 +12,13 @@ public final class Services {
|
|||
|
||||
public let authenticator: Authenticator
|
||||
public let dataService: DataService
|
||||
public let audioSession: AudioSession
|
||||
|
||||
public init(appEnvironment: AppEnvironment = PublicValet.storedAppEnvironment ?? .initialAppEnvironment) {
|
||||
let networker = Networker(appEnvironment: appEnvironment)
|
||||
self.authenticator = Authenticator(networker: networker)
|
||||
self.dataService = DataService(appEnvironment: appEnvironment, networker: networker)
|
||||
self.audioSession = AudioSession()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// MiniPlayer.swift
|
||||
//
|
||||
//
|
||||
// Created by Jackson Harper on 8/15/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Services
|
||||
import SwiftUI
|
||||
|
||||
public struct MiniPlayer: View {
|
||||
@EnvironmentObject var audioSession: AudioSession
|
||||
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
||||
|
||||
private let presentingView: AnyView
|
||||
|
||||
init<PresentingView>(
|
||||
presentingView: PresentingView
|
||||
) where PresentingView: View {
|
||||
self.presentingView = AnyView(presentingView)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .center) {
|
||||
presentingView
|
||||
VStack {
|
||||
Spacer()
|
||||
if self.audioSession.state != .stopped {
|
||||
HStack {
|
||||
Text("Title of the playing content")
|
||||
.font(.appCallout)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.frame(width: geometry.size.width, height: 108)
|
||||
.animation(.spring(), value: true)
|
||||
.background(Color.systemBackground)
|
||||
.shadow(color: .gray, radius: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func miniPlayer() -> some View {
|
||||
MiniPlayer(presentingView: self)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ public struct RootView: View {
|
|||
InnerRootView(viewModel: viewModel)
|
||||
.environmentObject(viewModel.services.authenticator)
|
||||
.environmentObject(viewModel.services.dataService)
|
||||
.environmentObject(viewModel.services.audioSession)
|
||||
.environment(\.managedObjectContext, viewModel.services.dataService.viewContext)
|
||||
.onChange(of: scenePhase) { phase in
|
||||
if phase == .background {
|
||||
|
|
@ -50,6 +51,7 @@ struct InnerRootView: View {
|
|||
.onAppear {
|
||||
viewModel.triggerPushNotificationRequestIfNeeded()
|
||||
}
|
||||
.miniPlayer()
|
||||
.snackBar(isShowing: $viewModel.showSnackbar, message: viewModel.snackbarMessage)
|
||||
// Schedule the dismissal every time we present the snackbar.
|
||||
.onChange(of: viewModel.showSnackbar) { newValue in
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ public final class RootViewModel: ObservableObject {
|
|||
@Published public var showPushNotificationPrimer = false
|
||||
@Published var snackbarMessage: String?
|
||||
@Published var showSnackbar = false
|
||||
@Published var showMiniPlayer = true
|
||||
|
||||
public init() {
|
||||
registerFonts()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// TextToSpeechViewModel.swift
|
||||
//
|
||||
//
|
||||
// Created by Jackson Harper on 8/15/22.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
|
||||
public enum TextToSpeechState {
|
||||
case stopped
|
||||
case paused
|
||||
case loading
|
||||
case playing
|
||||
}
|
||||
|
||||
public class AudioSession: ObservableObject {
|
||||
@Published public var state: TextToSpeechState = .stopped
|
||||
@Published public var audioUrl: URL?
|
||||
@Published public var title: String?
|
||||
|
||||
var timer: Timer?
|
||||
var player: AVAudioPlayer?
|
||||
|
||||
deinit {
|
||||
print("CLOSING DOWN THE SPEECH VIEW MODEL")
|
||||
self.player?.stop()
|
||||
}
|
||||
|
||||
func startAudio() {
|
||||
// Just simulating some loading delay here
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
|
||||
self.audioUrl = Bundle.main.url(forResource: "speech-sample", withExtension: "mp3")!
|
||||
|
||||
if let url = self.audioUrl {
|
||||
do {
|
||||
self.player = try AVAudioPlayer(contentsOf: url)
|
||||
if self.player?.play() ?? false {
|
||||
self.state = .playing
|
||||
self.startTimer()
|
||||
}
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startTimer() {
|
||||
if timer == nil {
|
||||
// Update every 100ms
|
||||
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(update(_:)), userInfo: nil, repeats: true)
|
||||
timer?.fire()
|
||||
}
|
||||
}
|
||||
|
||||
func stopTimer() {
|
||||
timer = nil
|
||||
}
|
||||
|
||||
// Every second, get the current playing time of the player and refresh the status of the player progressslider
|
||||
@objc func update(_: Timer) {
|
||||
if let player = player, player.isPlaying {
|
||||
print("play time in ms: ", Int(player.currentTime * 1000))
|
||||
}
|
||||
}
|
||||
|
||||
func pause() -> Bool {
|
||||
if let player = player {
|
||||
player.pause()
|
||||
state = .paused
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func playAudio() -> Bool {
|
||||
if let player = player {
|
||||
player.play()
|
||||
state = .playing
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ struct WebReaderContainerView: View {
|
|||
@State var annotation = String()
|
||||
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@EnvironmentObject var audioSession: AudioSession
|
||||
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
|
||||
@StateObject var viewModel = WebReaderViewModel()
|
||||
|
||||
|
|
@ -55,24 +56,14 @@ struct WebReaderContainerView: View {
|
|||
}
|
||||
}
|
||||
|
||||
static var player: AVAudioPlayer?
|
||||
|
||||
func playSound() {
|
||||
guard let url = Bundle.main.url(forResource: "speech-sample", withExtension: "mp3") else { return }
|
||||
|
||||
do {
|
||||
print("starting to play audio")
|
||||
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
let player = try AVAudioPlayer(contentsOf: url)
|
||||
//
|
||||
// player.enableRate = true
|
||||
// player.rate = 1.5
|
||||
player.play()
|
||||
|
||||
} catch {
|
||||
print("error playing audio", error.localizedDescription)
|
||||
var ttsButtonIcon: String {
|
||||
switch audioSession.state {
|
||||
case .stopped, .paused:
|
||||
return "play"
|
||||
case .playing:
|
||||
return "pause"
|
||||
case .loading:
|
||||
return "hourglass.circle"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,14 +85,20 @@ struct WebReaderContainerView: View {
|
|||
if FeatureFlag.enableTextToSpeechButton {
|
||||
Button(
|
||||
action: {
|
||||
// guard let htmlContent = item.htmlContent else { return }
|
||||
// let synthesizer = AVSpeechSynthesizer()
|
||||
// synthesizer.speak(AVSpeechUtterance(string: htmlContent))
|
||||
// synthesizer.sp
|
||||
playSound()
|
||||
switch audioSession.state {
|
||||
case .loading:
|
||||
// Do nothing here
|
||||
break
|
||||
case .stopped:
|
||||
audioSession.startAudio()
|
||||
case .paused:
|
||||
audioSession.playAudio()
|
||||
case .playing:
|
||||
audioSession.pause()
|
||||
}
|
||||
},
|
||||
label: {
|
||||
Image(systemName: "play")
|
||||
Image(systemName: ttsButtonIcon)
|
||||
.font(.appTitleTwo)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// AudioSession.swift
|
||||
//
|
||||
//
|
||||
// Created by Jackson Harper on 8/15/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum AudioSessionState {
|
||||
case stopped
|
||||
case paused
|
||||
case loading
|
||||
case playing
|
||||
}
|
||||
|
||||
// Our observable object class
|
||||
public class AudioSession: ObservableObject {
|
||||
@Published var state: AudioSessionState = .stopped
|
||||
@Published var title = "This is the title of my session"
|
||||
}
|
||||
Loading…
Reference in a new issue