Home > Blockchain >  How to cache a .mp3 from JSON in Swift?
How to cache a .mp3 from JSON in Swift?

Time:01-16

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 ?? "")
                }
            }

CodePudding user response:

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)
  • Related