Search code examples
swiftavaudioplayer

How to cache a .mp3 from JSON in Swift?


I have a function that downloads mp3 file from URL, passes it to AVAudioPlayer and then plays it in PlayerView. I want to implement a feature. When a mp3 will be downloaded, I want to be cached in the app files so If I open it later It wouldn't be downloaded. I saw tutorials of how to do this with Images, but not with mp3. How can this be created?


// Audio Manager itself

import Foundation
import AVFoundation
import AVFAudio

final class AudioManager: ObservableObject {
    
    // static let shared = AudioManager()
    
    var player: AVAudioPlayer?
    @Published private(set) var isPlaying: Bool = false {
        didSet {
            print(isPlaying, "isPlaying")
        }
    }
    
    func startPlayer(track: String) {
        
        guard let fileURL = URL(string: track) else { return  }
        
        do {
            
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
            
            let soundData = try Data(contentsOf: fileURL)
            self.player = try AVAudioPlayer(data: soundData)
            guard let player = player else { return }
            player.prepareToPlay()
            player.play()
            isPlaying = true
        }
        catch {
            
            print(error)
            
        }
    }
    
    func playPause() {
        
        guard let player = player else {
            print("Audio player not found")
            return
        }
        
        if player.isPlaying {
            player.pause()
            isPlaying = false

        } else {
            player.play()
            isPlaying = true

        }
        
    }
    
    func stop() {
        guard let player = player else {
            print("Audio player not found")
            return
        }
        
        if player.isPlaying {
            player.stop()
            isPlaying = false
        }
        
    }
    
}

// Main thing in my PlayerView. Passes the track to the audioManager
.onAppear {
                //                AudioManager.shared.startPlayer(track: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3")
                DispatchQueue.main.async {
                    audioManager.startPlayer(track: track ?? "")
                }
            }


Solution

  • A simple way to do this would just be to write the Data that you download straight to a file. The next time you try to play that track, check if a file for it exists and load that local file instead.

    Here's a (fairly naive) example:

    final class AudioManager: ObservableObject {
        
        // static let shared = AudioManager()
        
        var player: AVAudioPlayer?
        @Published private(set) var isDownloading = false
        @Published private(set) var isPlaying: Bool = false
        
    
        // MainActor so it always runs on the main queue
        @MainActor func startPlayer(track: String) async {
            
            guard let url = URL(string: track) else { return  }
            
            do {
                
                try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
                try AVAudioSession.sharedInstance().setActive(true)
    
                
                let songName = url.lastPathComponent
                var soundData: Data
                let tracksFolderUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).last!.appendingPathComponent("tracks")
                let trackUrl = tracksFolderUrl.appendingPathComponent(songName)
                
                if FileManager.default.fileExists(atPath: trackUrl.path) {
                    // Load local data if it exists
                    print("Loading data from \(trackUrl)")
                    soundData = try Data(contentsOf: trackUrl)
                } else {
    
                    //… otherwise load from network
                    isDownloading = true
                    print("Downloading data from \(url)")
                    (soundData, _) = try await URLSession.shared.data(from: url)
    
                    //… then save to disk
                    try FileManager.default.createDirectory(at: tracksFolderUrl, withIntermediateDirectories: true)
                    print("Saving data to \(trackUrl)")
                    try soundData.write(to: trackUrl)
                    isDownloading = false
                }
                
                self.player = try AVAudioPlayer(data: soundData)
                guard let player = player else { return }
                player.prepareToPlay()
                player.play()
                isPlaying = true
            }
            catch {            
                print(error)
                
            }
        }
    }
    
    struct ContentView: View {
        
        @StateObject var audioManager = AudioManager()
        
        var body: some View {
            ZStack {
                if audioManager.isDownloading {
                    VStack {
                        Text("Downloading")
                        ProgressView()
                    }
                } else {
                    Text("Playing")
                }
            }
            .task {
                await audioManager.startPlayer(track: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3")
            }
        }
    }
    
    
    

    Note that I've made the startPlayer func async so it doesn't block the main thread and used a different method to download the data

    try await URLSession.shared.data(from: url)