SwiftUI AVPlayer Error Reporting/Handling - swift

I've created a media app, which plays music via HTTP live stream in SwiftUI. The code (snippet) looks like following:
struct PlayerView: View {
#State var audioPlayer: AVPlayer!
#State var buttonSymbol: String = "pause.circle.fill"
let url: URL!
var body: some View {
VStack {
Text("Sound")
HStack {
Button(action: {
if self.audioPlayer.rate != 0.0 {
self.buttonSymbol = "play.circle.fill"
self.audioPlayer.pause()
} else {
self.buttonSymbol = "pause.circle.fill"
self.audioPlayer.play()
}
}) {
Image(systemName: self.buttonSymbol)
}
.buttonStyle(PlainButtonStyle())
}
}
.onAppear {let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSession.Category.playback, mode: .default, policy: AVAudioSession.RouteSharingPolicy.longFormAudio, options: [])
} catch let error {
fatalError("*** Unable to set up the audio session: \(error.localizedDescription)")
}
let item = AVPlayerItem(url: self.url)
self.audioPlayer = AVPlayer(playerItem: item)
audioSession.activate(options: []) { (success, error) in
guard error == nil else {
fatalError("An error occurred: \(error!.localizedDescription)")
return
}
self.audioPlayer.play()
}
}
}
}
My lack of understanding is, how do I receive the underlying AVPlayer/AVPlayerItem errors (for instance the HTTP stream is interrupted) in SwiftUI in order to show them in the UI? Which SwiftUI action I have to use and how?

According to the documentation AVPlayerItem has a notification called AVPlayerItemFailedToPlayToEndTime.
One possible implementation in a Viewmodel would be:
import Combine
....
playerItemFailedToPlayToEndTimeObserver = NotificationCenter
.default
.publisher(for: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime)
.sink(receiveValue: {[weak self] notification in
// handle notification here
})
You would need to hold a reference to playerItemFailedToPlayToEndTimeObserver else it will go out of scope as soon as the method calling this finished.

Related

How to make a swifui button action repeat in a loop after one click of the button

I'm relatively new to swift and am I'm making a swiftui calling application with a deepfaked chatbot that requires me to transcribe the users speech to text and then play an appropriate response.
I currently have a working flow that that starts a speech recognition session when the user clicks a button, and stops the recording/recognition when the user clicks the button again. They need to keep clicking start/stop in order for this to work.
To make this hands free like a real voice chat app, I would like to get rid of requiring the user to click buttons. I would like them to click a "call" button once to get the recording and the speech recognition going, and then automatically detect when they stop talking with a 2 second timer. Then I can send the text to the backend and I would like to automatically restart the mic and the speech recognition so I can keep doing this in a loop to partition user input, until the user clicks the button again to hang up.
I have implemented a timer to detect when the user stops speaking, but when I try to restart the microphone and the speech recognition session using a repeat while loop, my program doesn't work as I expect and speech recognition doesn't work.
This is what I tried to do to make the "addItem" logic run in a loop once the user clicks the call button initially. The logic to end speech recognition after 2 seconds of silence works fine, but as soon as I add the repeat while loop, the program goes haywire after the first click of the call button. I can't figure out the proper way to make the logic loop after speech recognition ends and I get the text.
Main View code:
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Todo.created, ascending: true)], animation: .default) private var todos: FetchedResults<Todo>
#State private var recording = false
#ObservedObject private var mic = MicMonitor(numberOfSamples: 30)
private var speechManager = SpeechManager()
var body: some View {
NavigationView {
ZStack(alignment: .bottomTrailing) {
List {
Text(todos.last?.text ?? "----")
}
.navigationTitle("Speech To Text")
VStack{
recordButton()
}
}.onAppear {
speechManager.checkPermissions()
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
private func recordButton() -> some View {
Button(action: addItem) {
Image(systemName: "phone.fill")
.font(.system(size: 40))
.padding()
.cornerRadius(10)
}.foregroundColor(recording ? .red : .green)
}
private func addItem() { //THIS IS THE FUNCTION THAT I WANT TO RUN IN A LOOP WITHOUT NEEDING TO CLICK THE BUTTON EVERYTIME
if speechManager.isRecording {
self.recording = false
mic.stopMonitoring()
speechManager.stopRecording()
} else {
repeat {
self.recording = true
mic.startMonitoring()
speechManager.start { (speechText) in
guard let text = speechText, !text.isEmpty else {
self.recording = false
return
}
print("FINAL TEXT AFTER TIMER ENDS: ", text)
DispatchQueue.main.async {
withAnimation {
let newItem = Todo(context: viewContext)
newItem.id = UUID()
newItem.text = text
newItem.created = Date()
do {
try viewContext.save()
} catch {
print(error)
}
mic.stopMonitoring() //At this point, I want to restart the recording and the speech recognition session and keep doing the else statement in a loop automatically }
}
}
} while self.recording == true
}
speechManager.isRecording.toggle()
print("Toggeled isRecording!!")
}
}
Speech Recognition code:
import Foundation
import Speech
class SpeechManager {
public var isRecording = false
private var audioEngine: AVAudioEngine!
private var inputNode: AVAudioInputNode!
private var audioSession: AVAudioSession!
var timer : Timer?
private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
func checkPermissions() {
SFSpeechRecognizer.requestAuthorization{ (authStatus) in
DispatchQueue.main.async {
switch authStatus {
case .authorized: break
default:
print("Speech recognition is not available")
}
}
}
}
func start(completion: #escaping (String?) -> Void) {
if isRecording {
//stopRecording()
} else {
startRecording(completion: completion)
}
}
func startRecording(completion: #escaping (String?) -> Void) {
//createTimer(4)
guard let recognizer = SFSpeechRecognizer(), recognizer.isAvailable else {
print("Speech recognition is not available")
return
}
recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
recognitionRequest!.shouldReportPartialResults = true
recognizer.recognitionTask(with: recognitionRequest!) { (result, error) in
//let defaultText = self.text
guard error == nil else {
print("got error \(error!.localizedDescription)")
return
}
guard let result = result else { return }
////////////////
self.timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { (timer) in
self.timer?.invalidate()
print("invalidated timer")
self.stopRecording()
return
////////////////
})
if result.isFinal {
completion(result.bestTranscription.formattedString)
print("FINAL")
print(result.bestTranscription.formattedString)
}
}
audioEngine = AVAudioEngine()
inputNode = audioEngine.inputNode
let recordingFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, _) in
self.recognitionRequest?.append(buffer)
}
audioEngine.prepare()
do {
audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.record, mode: .spokenAudio, options: .duckOthers)
try audioSession.setActive(true, options:.notifyOthersOnDeactivation)
try audioEngine.start()
} catch {
print(error)
}
}
func stopRecording() {
audioEngine.stop()
recognitionRequest?.endAudio()
recognitionRequest = nil
inputNode.removeTap(onBus: 0)
audioSession = nil
}
}

SwiftUI: Error validating an if based on whether or not there is an instance

I am making a music player in swiftui, there is a part of the code where an instance of the player (audioManager.player) on which several components depend, in order not to set the same value constantly I wrapped the playback part in a conditional (if let player = audioManager.player) based on whether or not there is a “player” instance, but when doing so the content disappears strangely, as if the “player” instance does not exist or is negative, I tried it just validating a part and I realized that everything works correctly but I don't know why the content is not displayed on the canvas or in the simulator. I would appreciate if you help me with a suggestion or solution, everything is welcome. ;)
import Foundation
import AVKit
final class AudioManager: ObservableObject {
var player: AVAudioPlayer?
func startPlayer(track: String, isPreview: Bool = false) {
guard let url = Bundle.main.url(forResource: track, withExtension: "mp3") else {
print("Resourse not found")
return
}
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url)
if isPreview {
player?.prepareToPlay()
} else {
player?.play()
}
} catch {
print("Fail to initialize", error)
}
}
}
import SwiftUI
struct PlayerView: View {
#EnvironmentObject var audioManager: AudioManager
#State private var value: Double = 0.0
#State private var isEditing: Bool = false
var isPreview: Bool = false
let timer = Timer
.publish(every: 0.5, on: .main, in: .common)
.autoconnect()
var body: some View {
ZStack {
VStack(alignment: .center, spacing: 50) {
VStack(spacing: 10) {
// MARK: ** THE ERROR IS HERE **
if let player = audioManager.player {
Slider(value: $value, in: 0...player.duration)
.foregroundColor(.white)
.padding(.top)
}
} // VStack
.padding(.horizontal, 30)
} // VStack
.foregroundColor(.white)
}
.onAppear {
audioManager.startPlayer(track: "meditation1", isPreview: isPreview)
}
.onReceive(timer) { _ in
guard let player = audioManager.player, !isEditing else { return }
value = player.currentTime
}
}
}
struct PlayerView_Previews: PreviewProvider {
static var previews: some View {
Group {
PlayerView(isPreview: true)
.environmentObject(AudioManager())
}
}
}

SwiftUI Firebase Authentication dismiss view after successfully login

I'm a beginner iOS developer and I have a problem with my first application. I'm using Firebase as a backend for my app and I have already sign in and sing up methods implemented. My problem is with dismissing LoginView after Auth.auth().signIn method finishing. I've managed to do this when I'm using NavigationLink by setting ObservableObject in isActive:
NavigationLink(destination: DashboardView(), isActive: $isUserLogin) { EmptyView() }
It's working as expected: when app ending login process screen is going to next view - Dashboard.
But I don't want to use NavigationLink and creating additional step, I want just go back to Dashboard using:
self.presentationMode.wrappedValue.dismiss()
In this case I don't know how to force app to wait till method loginUser() ends. This is how my code looks now:
if loginVM.loginUser() {
appSession.isUserLogin = true
self.presentationMode.wrappedValue.dismiss()
}
I've tried to use closures but it doesn't work or I'm doing something wrong.
Many thanks!
You want to use a AuthStateDidChangeListenerHandle and #EnvrionmentObject, like so:
class SessionStore: ObservableObject {
var handle: AuthStateDidChangeListenerHandle?
#Published var isLoggedIn = false
#Published var userSession: UserModel? { didSet { self.willChange.send(self) }}
var willChange = PassthroughSubject<SessionStore, Never>()
func listenAuthenticationState() {
handle = Auth.auth().addStateDidChangeListener({ [weak self] (auth, user) in
if let user = user {
let firestoreUserID = API.FIRESTORE_DOCUMENT_USER_ID(userID: user.uid)
firestoreUserID.getDocument { (document, error) in
if let dict = document?.data() {
//Decoding the user, you can do this however you see fit
guard let decoderUser = try? UserModel.init(fromDictionary: dict) else {return}
self!.userSession = decoderUser
}
}
self!.isLoggedIn = true
} else {
self!.isLoggedIn = false
self!.userSession = nil
}
})
}
func logOut() {
do {
try Auth.auth().signOut()
print("Logged out")
} catch let error {
debugPrint(error.localizedDescription)
}
}
func unbind() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit {
print("deinit - seession store")
}
}
Then simply do something along these lines:
struct InitialView: View {
#EnvironmentObject var session: SessionStore
func listen() {
session.listenAuthenticationState()
}
var body: some View {
ZStack {
Color(SYSTEM_BACKGROUND_COLOUR)
.edgesIgnoringSafeArea(.all)
Group {
if session.isLoggedIn {
DashboardView()
} else if !session.isLoggedIn {
SignInView()
}
}
}.onAppear(perform: listen)
}
}
Then in your app file, you'd have this:
InitialView()
.environmentObject(SessionStore())
By using an #EnvironmentObject you can now access the user from any view, furthermore, this also allows to track the Auth status of the user meaning if they are logged in, then the application will remember.

SwiftUI Video player how to set url

I am trying to create a view that allows me to send a URL of the video I want to play. My view looks like this:
import SwiftUI
import AVKit
struct PlayerView: View {
var videoURL : String
private let player = AVPlayer(url: URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!)
var body: some View {
VideoPlayer(player: player)
.onAppear() {
// Start the player going, otherwise controls don't appear
player.play()
}
.onDisappear() {
// Stop the player when the view disappears
player.pause()
}
}
}
If try to set the URL to the passed parameter I get this error:
Cannot use instance member 'videoURL' within property initializer; property initializers run before 'self' is available
How do I pass aURL to the View to play different movies?
I changed my code as suggested and the video does not play:
import SwiftUI
import AVKit
struct PlayerView: View {
var videoURL : String
#State private var player : AVPlayer?
var body: some View {
VideoPlayer(player: player)
.onAppear() {
// Start the player going, otherwise controls don't appear
guard let url = URL(string: videoURL) else {
return
}
print(url)
let player = AVPlayer(url: url)
self.player = player
self.player?.seek(to: CMTime.zero)
self.player!.play()
}
.onDisappear() {
// Stop the player when the view disappears
player?.pause()
}
}
}
I am trying to change the video on the timer. I tried this:
} else {
VideoPlayer(player: viewModel.player)
.onAppear {
viewModel.urlString = "https://testsite.neumont.edu/images/green.mp4"
}
.onReceive(movieSwitchTimer) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
viewModel.urlString = "https://testsite.neumont.edu/images/earth.mp4"
}
self.activeImageIndex = (self.activeImageIndex + 1) % slideStore.images.count
}
}
There are a number of ways to achieve this, but since you're already using onAppear, that seems like a good place to accomplish this:
struct PlayerView: View {
var videoURL : String
#State private var player : AVPlayer?
var body: some View {
VideoPlayer(player: player)
.onAppear() {
// Start the player going, otherwise controls don't appear
guard let url = URL(string: videoURL) else {
return
}
let player = AVPlayer(url: url)
self.player = player
player.play()
}
.onDisappear() {
// Stop the player when the view disappears
player?.pause()
}
}
}
You'll want player to be #State because it should persist between renders of the view.
Update, based on comments, handling a scenario where the URL may change:
class VideoViewModel : ObservableObject {
#Published var urlString : String? {
didSet {
guard let urlString = urlString, let url = URL(string: urlString) else {
return
}
player = AVPlayer(url: url)
player.seek(to: .zero)
player.play()
}
}
var player = AVPlayer()
}
struct ContentView : View {
#StateObject var viewModel = VideoViewModel()
var body: some View {
VideoPlayer(player: viewModel.player)
.onAppear {
viewModel.urlString = "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8"
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
viewModel.urlString = Bundle.main.url(forResource: "IMG_0226", withExtension: "mp4")!.absoluteString
}
}
}
}
In the update, you can see that I store the player in an ObservableObject. Any time the url string gets changed, it reloads the player. The onAppear is not necessary in your code -- it's just a way to show loading different videos at different times (the first being the URL you provided, the second being a URL from my bundle).

Using ASWebAuthentication in SwiftUI

Having a bit of trouble getting authentication to work from within a SwiftUI view. I’m using ASWebAuthentication and whenever I run I get an error:
Cannot start ASWebAuthenticationSession without providing presentation context. Set presentationContextProvider before calling -start.
I’m creating a ViewController and passing in a reference to the Scene Delegate window based on this stack overflow post but that answer doesn’t seem to be working for me. I’ve also found this reddit post, but I’m a little unclear as to how they were able to initialize the view with the window before the scene delegate’s window is set up.
This is the code I’m using for the SwiftUI view:
import SwiftUI
import AuthenticationServices
struct Spotify: View {
var body: some View {
Button(action: {
self.authWithSpotify()
}) {
Text("Authorize Spotify")
}
}
func authWithSpotify() {
let authUrlString = "https://accounts.spotify.com/authorize?client_id=\(spotifyID)&response_type=code&redirect_uri=http://redirectexample.com/callback&scope=user-read-private%20user-read-email"
guard let url = URL(string: authUrlString) else { return }
let session = ASWebAuthenticationSession(
url: url,
callbackURLScheme: "http://redirectexample.com/callback",
completionHandler: { callback, error in
guard error == nil, let success = callback else { return }
let code = NSURLComponents(string: (success.absoluteString))?.queryItems?.filter({ $0.name == "code" }).first
self.getSpotifyAuthToken(code)
})
session.presentationContextProvider = ShimViewController()
session.start()
}
func getSpotifyAuthToken(_ code: URLQueryItem?) {
// Get Token
}
}
struct Spotify_Previews: PreviewProvider {
static var previews: some View {
Spotify()
}
}
class ShimViewController: UIViewController, ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return globalPresentationAnchor ?? ASPresentationAnchor()
}
}
And in the SceneDelegate:
var globalPresentationAnchor: ASPresentationAnchor? = nil
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Use a UIHostingController as window root view controller
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: Spotify())
self.window = window
window.makeKeyAndVisible()
}
globalPresentationAnchor = window
}
Any idea how I can make this work?
Regarding the reddit post, I got it to work as is. My misunderstanding was that the AuthView isn't being used as an 'interface' View. I created a normal SwiftUI View for my authentication view, and I have a Button with the action creating an instance of the AuthView, and calling the function that handles the session. I'm storing the globalPositionAnchor in an #EnvironmentObject, but you should be able to use it from the global variable as well. Hope this helps!
struct SignedOutView: View {
#EnvironmentObject var contentManager: ContentManager
var body: some View {
VStack {
Text("Title")
.font(.largeTitle)
Spacer()
Button(action: {AuthProviderView(window: self.contentManager.globalPresentationAnchor!).signIn()}) {
Text("Sign In")
.padding()
.foregroundColor(.white)
.background(Color.orange)
.cornerRadius(CGFloat(5))
.font(.headline)
}.padding()
}
}
}
I've run into something similar before when implementing ASWebAuthenticationSession. One thing I didn't realize, is you have to have a strong reference to the session variable. So I would make you session variable a property of your class and see if that fixes the issue. A short snippet of what I mean:
// initialize as a property of the class
var session: ASWebAuthenticationSession?
func authWithSpotify() {
let authUrlString = "https://accounts.spotify.com/authorize?client_id=\(spotifyID)&response_type=code&redirect_uri=http://redirectexample.com/callback&scope=user-read-private%20user-read-email"
guard let url = URL(string: authUrlString) else { return }
// assign session here
session = ASWebAuthenticationSession(url: url, callbackURLScheme: "http://redirectexample.com/callback", completionHandler: { callback, error in
guard error == nil, let success = callback else { return }
let code = NSURLComponents(string: (success.absoluteString))?.queryItems?.filter({ $0.name == "code" }).first
self.getSpotifyAuthToken(code)
})
session.presentationContextProvider = ShimViewController()
session.start()
}
Ronni -
I ran into the same problem but finally got the ShimController() to work and avoid the warning. I got sucked into the solution but forgot to instantiate the class. Look for my "<<" comments below. Now the auth is working and the callback is firing like clockwork. The only caveat here is I'm authorizing something else - not Spotify.
var session: ASWebAuthenticationSession?
var shimController = ShimViewController() // << instantiate your object here
func authWithSpotify() {
let authUrlString = "https://accounts.spotify.com/authorize?client_id=\(spotifyID)&response_type=code&redirect_uri=http://redirectexample.com/callback&scope=user-read-private%20user-read-email"
guard let url = URL(string: authUrlString) else { return }
// assign session here
session = ASWebAuthenticationSession(url: url, callbackURLScheme: "http://redirectexample.com/callback", completionHandler: { callback, error in
guard error == nil, let success = callback else { return }
let code = NSURLComponents(string: (success.absoluteString))?.queryItems?.filter({ $0.name == "code" }).first
self.getSpotifyAuthToken(code)
})
session.presentationContextProvider = shimController // << then reference it here
session.start()
}
Using .webAuthenticationSession(isPresented:content) modifier in BetterSafariView, you can easily start a web authentication session in SwiftUI. It doesn't need to hook SceneDelegate.
import SwiftUI
import BetterSafariView
struct SpotifyLoginView: View {
#State private var showingSession = false
var body: some View {
Button("Authorize Spotify") {
self.showingSession = true
}
.webAuthenticationSession(isPresented: $showingSession) {
WebAuthenticationSession(
url: URL(string: "https://accounts.spotify.com/authorize")!,
callbackURLScheme: "myapp"
) { callbackURL, error in
// Handle callbackURL
}
}
}
}