Setup a single AudioSession, a little improvement on the MiniPlayer UI

This commit is contained in:
Jackson Harper 2022-08-15 23:06:29 +08:00
parent 06036b7517
commit d918b8b0a8
5 changed files with 149 additions and 113 deletions

View file

@ -27,17 +27,52 @@ public struct MiniPlayer: View {
presentingView
VStack {
Spacer()
if self.audioSession.state != .stopped {
if let item = audioSession.item, self.audioSession.state != .stopped {
HStack {
Text("Title of the playing content")
Text(item.unwrappedTitle)
.font(.appCallout)
Spacer()
.lineSpacing(1.25)
.foregroundColor(.appGrayTextContrast)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
Button(
action: {
switch audioSession.state {
case .playing:
audioSession.pause()
case .paused:
audioSession.unpause()
default:
break
}
},
label: {
Image(systemName: audioSession.state == .playing ? "pause.circle" : "play.circle")
.font(.appTitleTwo)
}
)
.frame(width: 28, height: 28)
Button(
action: {
audioSession.stop()
},
label: {
Image(systemName: "xmark")
.font(.appTitleTwo)
}
)
.frame(width: 28, height: 28)
}
.padding()
.frame(width: geometry.size.width, height: 108)
.frame(width: geometry.size.width, height: 88) // this should be 108 once we add GrabberVisible at the bottom
.animation(.spring(), value: true)
.background(Color.systemBackground)
.shadow(color: .gray, radius: 2)
.tint(.appGrayTextContrast)
.background(
Color.systemBackground
.shadow(color: .gray.opacity(0.33), radius: 8, x: 0, y: 4)
.mask(Rectangle().padding(.top, -20))
)
}
}
}

View file

@ -1,85 +0,0 @@
//
// 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
}
}

View file

@ -56,17 +56,6 @@ struct WebReaderContainerView: View {
}
}
var ttsButtonIcon: String {
switch audioSession.state {
case .stopped, .paused:
return "play"
case .playing:
return "pause"
case .loading:
return "hourglass.circle"
}
}
var navBar: some View {
HStack(alignment: .center) {
#if os(iOS)
@ -86,19 +75,24 @@ struct WebReaderContainerView: View {
Button(
action: {
switch audioSession.state {
case .loading:
// Do nothing here
break
case .stopped:
audioSession.startAudio()
case .paused:
audioSession.playAudio()
case .playing:
audioSession.pause()
if audioSession.item == self.item {
audioSession.pause()
return
}
fallthrough
case .paused:
if audioSession.item == self.item {
audioSession.unpause()
return
}
fallthrough
default:
audioSession.play(item: self.item)
}
},
label: {
Image(systemName: ttsButtonIcon)
Image(systemName: audioSession.isPlayingItem(item: item) ? "pause.circle" : "play.circle")
.font(.appTitleTwo)
}
)

View file

@ -5,8 +5,12 @@
// Created by Jackson Harper on 8/15/22.
//
import AVFoundation
import Foundation
import Models
import Utils
public enum AudioSessionState {
case stopped
case paused
@ -16,6 +20,94 @@ public enum AudioSessionState {
// Our observable object class
public class AudioSession: ObservableObject {
@Published var state: AudioSessionState = .stopped
@Published var title = "This is the title of my session"
@Published public var state: AudioSessionState = .stopped
@Published public var item: LinkedItem?
@Published public var audioUrl: URL?
var timer: Timer?
var player: AVAudioPlayer?
public init() {}
public func play(item: LinkedItem) {
// Stop any existing session
stop()
self.item = item
startAudio()
}
public func stop() {
player?.stop()
timer = nil
player = nil
item = nil
}
public func isPlayingItem(item: LinkedItem) -> Bool {
state == .playing && self.item == item
}
public func startAudio() {
state = .loading
// Just simulating some loading delay here
DispatchQueue.main.asyncAfter(deadline: .now()) {
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)
self.state = .stopped
}
}
}
}
public func pause() -> Bool {
if let player = player {
player.pause()
state = .paused
return true
}
return false
}
public func unpause() -> Bool {
playAudio()
}
public func playAudio() -> Bool {
if let player = player {
player.play()
state = .playing
return true
}
return false
}
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))
}
}
}

Binary file not shown.