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)