How can I play a sound from the asset catalog using AVAudioPlayerNode? - swift

I'm trying to use an AVAudioPlayerNode to play sounds from the Assets.xcassets asset catalog, but I can't figure out how to do it.
I've been using AVAudioPlayer, which can be initialized with an NSDataAsset like this:
let sound = NSDataAsset(name: "beep")!
do {
let player = try AVAudioPlayer(data: sound.data, fileTypeHint: AVFileTypeWAVE)
player.prepareToPlay()
player.play()
} catch {
print("Failed to create AVAudioPlayer")
}
I want to use an AVAudioPlayerNode instead (for pitch shifting and other reasons). I can create the engine and hook up the node OK:
var engine = AVAudioEngine()
func playSound(named name: String) {
let mixer = engine.mainMixerNode
let playerNode = AVAudioPlayerNode()
engine.attach(playerNode)
engine.connect(playerNode, to: mixer, format: mixer.outputFormat(forBus: 0))
// play the file (this is what I don't know how to do)
}
It looks like the method to use for playing the file is playerNode.scheduleFile(). It takes an AVAudioFile, so I thought I'd try to make one. But the initializer for AVAudioFile wants a URL. As far as I can tell, assets in the asset catalog are not available by URL. I can get the data directly using NSDataAsset, but there doesn't seem to be any way to use it to populate an AVAudioFile.
Is it possible to play sounds from the asset catalog with an AVAudioPlayerNode? And if so, how?

OK so your problem is that you would like to get a URL from a file in your Asset catalog right?
I've looked around but only found this answer
As it says
It basically just gets image from assets, saves its data to disk and return file URL
You should probably change it to look for MP3 files (or WAV or whatever you prefer, maybe that could be an input parameter)
So you could end up with something like:
enum SoundType: String {
case mp3 = "mp3"
case wav = "wav"
}
class AssetExtractor {
static func createLocalUrl(forSoundNamed name: String, ofType type: SoundType = .mp3) -> URL? {
let fileManager = FileManager.default
let cacheDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let url = cacheDirectory.appendingPathComponent("\(name).\(type)")
guard fileManager.fileExists(atPath: url.path) else {
guard
let image = UIImage(named: name),
let data = UIImagePNGRepresentation(image)
else { return nil }
fileManager.createFile(atPath: url.path, contents: data, attributes: nil)
return url
}
return url
}
}
Maybe a bit far fetched but I haven't found any other options.
Hope that helps you.

Related

Writing iOS application logs on a text file

I am using this answer to log messages in my app.
import Foundation
class Log: TextOutputStream {
func write(_ string: String) {
let fm = FileManager.default
let log = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("log.txt")
if let handle = try? FileHandle(forWritingTo: log) {
handle.seekToEndOfFile()
handle.write(string.data(using: .utf8)!)
handle.closeFile()
} else {
do {
try string.data(using: .utf8)?.write(to: log)
} catch {
print(error)
}
}
}
static var log: Log = Log()
private init() {}
}
Used as follows using the Singleton pattern,
print("\(#function) Test Log", to: &Log.log)
This would append the String to the log.txt file. I cannot see the file being created in the Files.app and it doesn't produce an error either. If I print the path of the file where it's being saved it shows,
file:///var/mobile/Containers/Data/Application/00EBA5E5-7132-495E-B90E-E6CF32BA3EA7/Documents/
Where should it be saved? Do I have to do any prior setup? I can't seem to make this work. Do I have to do do something before to create the folder? Nothing shows up in the Files.app.
EDIT: I am not using the Simulator, I need to use a real device.
Okay I got confused and I totally forgot this document is not supposed to show up in the Files.app. It's stored inside the app's container. If you want to share it from the documents sheet and send it to another device via AirDrop or whatever add this action to trigger when you tap a button intended to share the document.
let fm = FileManager.default
let fileUrl = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("log.txt")
var filesToShare = [Any]()
filesToShare.append(fileUrl)
let activityViewController = UIActivityViewController(activityItems: filesToShare, applicationActivities: nil)
self.present(activityViewController, animated: true, completion: nil)

Swift - Check if the file download from a URL is completed

I am downloading mp4 clips from a M3U8 manifest which can have around 700 clips. Everything works fine to download them but what would be the best to check individual downloads are finished? When all the clips are downloaded, I merge them into one but I need to know when all my clips have been downloaded first.
This is the code snippet I use to download the video clip.
func download(video: String){
DispatchQueue.global(qos: .background).async {
if let url = URL(string: "http://SERVER/storage/sessions/SESSIONID/mp4_segments/\(video)"),
let urlData = NSData(contentsOf: url) {
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0];
let fileName = video
let filePath = "\(documentsPath)/SegmentVideos/\(fileName)"
urlData.write(toFile: filePath, atomically: true)
}
}
}
This is the code snippet that reads the M3U8 file and splits it so I can grab the video clip's name.
func checkM3U8forClips(){
guard let url = url else {return}
do {
let contents = try String(contentsOf: url)
let splitContent = contents.components(separatedBy: "\n")
for split in splitContent {
if split.hasSuffix("mp4") {
download(video: split)
}
}
} catch {
print("error with mp4 segments: \(error.localizedDescription)")
}
}
One reason you are in a quandary is that this code is wrong:
if let url = URL(string: "http://SERVER/storage/sessions/SESSIONID/mp4_segments/\(video)"),
let urlData = NSData(contentsOf: url) {
You must never use NSData(contentsOf:) to do networking. If you want to network, then network: use URLSession and a proper data task or download task. Now you get the callbacks you need; if you do it in the full form you get a full set of delegate callbacks that tell you exactly when a download has succeeded and completed (or failed).
As for your overall question, i.e. how can I know when multiple asynchronous operations have all finished, that is what things like DispatchGroup, or operation dependencies, or the new Combine framework are for.

Read Gallery Video in Custom Size

How could I get a video from Gallery(Photos) in custom format and size.
for example I want to read a video in 360p.
I used below code to get video data but apple said it doesn't guarantee to read it in lowest quality.
It's a PHAsset extension, so self refering to a PHAsset object.
var fileData: Data? = nil
let manager = PHImageManager.default()
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
options.deliveryMode = .fastFormat
manager.requestAVAsset(forVideo: self, options: options) {
(asset: AVAsset?, audioMix: AVAudioMix?, _) in
if let avassetURL = asset as? AVURLAsset {
guard let video = try? Data(contentsOf: avassetURL.url) else {
print("reading video failed")
return
}
fileData = video
}
}
There is a simple reason it can't be guaranteed: The file in 360p might not be on the device or in the cloud. So the Photos framework will deliver a format nearest to what you request. If you want exactly 360p, I would recommend you reencode the video you get from the photos framework yourself.

Reading in array of elements from macOS directory

I can read in elements (images or videos) uploaded in Xcode's project this way:
let photos = (1...9).map {
NSImage(named: NSImage.Name(rawValue: "000\($0)"))
}
or like this:
let videos = (1...100).map { _ in
Bundle.main.urls(forResourcesWithExtension: "mov", subdirectory: nil)![Int(arc4random_uniform(UInt32(100)))]
}
But how to read in files (as array) from macOS directory using .map method?
/Users/me/Desktop/ArrayOfElements/
First of all your second way is very expensive, the array of movie URLs is read a hundred times from the bundle. This is more efficient:
let resources = Bundle.main.urls(forResourcesWithExtension: "mov", subdirectory: nil)!
let videos = (1...100).map { _ in
resources[Int(arc4random_uniform(100))]
}
Reading from /Users/me/Desktop is only possible if the application is not sandboxed, otherwise you can only read form the application container.
To get all files (as [URL]) from a directory use FileManager:
let url = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Desktop/ArrayOfElements")
do {
let fileURLs = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
let movieURLs = fileURLs.filter{ $0.pathExtension == "mov" }
print(movieURLs)
} catch { print(error) }
Rather than using map I recommend to implement an Array extension adding shuffle()

Load temp video and play

I need to play continuously a video stored inside the temp directory.
func setVideo(url vid: String!) {
let directory = NSTemporaryDirectory()
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("tempMovie\(Date().timeIntervalSince1970)").appendingPathExtension("mp4")
let tempFile = NSURL.fileURL(withPathComponents: [directory])
let file = vid.components(separatedBy: ".")
guard let path = Bundle.main.path(forResource: "\(tempURL)", ofType:file[2]) else {
debugPrint( "\(file.joined(separator: ".")) not found with path: \(file[0] + "." + file[1])")
return
}
let player = AVPlayer(url: URL(fileURLWithPath: path))
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.view.bounds
self.videoView.layer.addSublayer(playerLayer)
player.play()
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player.currentItem, queue: nil, using: { (_) in
DispatchQueue.main.async {
self.player?.seek(to: kCMTimeZero)
self.player?.play()
}
})
}
The video location and file url is:
file:///private/var/mobile/Containers/Data/Application/E7D12401-F147-4905-83BE-72909F91E004/tmp/tempMovie1501672791.33525.mp4
The continuous loop part works from a previous project however I can't seem to get the temp stored video to load inside the player. I've tried to get the temp directory manually but it didn't work.
Moving the video from tmp to Documents directory worked for me.
This is how you do it:
let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let destinationURL = documentsDirectoryURL.appendingPathComponent("filename.fileExtension")
do {
try FileManager.default.moveItem(at: previousTempLocation, to: destinationURL)
} catch let error as NSError { print(error.localizedDescription)}
let player = AVPlayer(url: destinationURL)
Apple says this on /tmp directory
Use this directory to write temporary files that do not need to
persist between launches of your app. Your app should remove files
from this directory when they are no longer needed; however, the
system may purge this directory when your app is not running.
Apple also recommends this for user data
Put user data in Documents/. User data generally includes any files
you might want to expose to the user—anything you might want the user
to create, import, delete or edit. Video and audio apps may even include
files that the user has downloaded to watch or listen to later.