I'm having trouble saving the filtered video to disk with GPUImage 2. I've added a filter successfully and now i need to save that video to disk, which is where i'm running into issues. There is more answers regarding this subject with GPUImage 1 but i'm not able to get them to work with GPUImage 2
The filtering:
let bundleURL = Bundle.main.resourceURL!
let movieURL = URL(string: "test.mov", relativeTo: bundleURL)
do {
movie = try MovieInput(url: movieURL!, playAtActualSpeed: true)
filter = HueAdjustment()
filter.hue = hue
movie --> filter --> renderView
movie.runBenchmark = true
movie.start()
} catch {
print("Couldn't process movie error: \(error)")
}
Trying to save it based on the sampleLiveRecording which doesn't seem to suit a filtered movie.
{
do {
let documentsDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let fileURL = URL(string: "testRender.mov", relativeTo: documentsDir)!
do {
try FileManager.default.removeItem(at: fileURL)
} catch {}
movieOutput = try MovieOutput(URL: fileURL, size: Size(width: 448, height: 426), liveVideo: false)
filter --> movieOutput!
movieOutput!.startRecording()
} catch {
fatalError("Couldn't Initialize Movie: \(error)")
}
}
Update: I didn't find an answer but I reverted to using GPUImage 1.
In my practice i've used this class for output video to file.
private class FileOutput {
enum State: Int {
case created
case recording
case finishing
}
var state: State = .created
let writingQueue = DispatchQueue(label: "FileOutputQueue", attributes: [])
var fileURI: URL
var duration: Double {
return fileOutput.recordDuration
}
var didFinishWriting: ( () -> () )? = nil
fileprivate let fileOutput: MovieOutput
init( camera: Camera, fileURI: URL ) throws {
do {
self.fileURI = fileURI
fileOutput = try MovieOutput(URL: fileURI,
size: Size(width: 1080, height: 1920),
liveVideo: true)
let movieOutputFilter = BasicOperation()
movieOutputFilter.overriddenOutputRotation = .rotate180
camera --> movieOutputFilter --> fileOutput
} catch {
throw NSError(domain: "com.test.camera", code: 1200000, userInfo: nil)
}
}
func start() {
if state == .created {
state = .recording
let _ = writingQueue.sync {
self.fileOutput.startRecording()
}
}
}
func finish() {
if state == .recording {
state = .finishing
let _ = writingQueue.sync {
self.fileOutput.finishRecording {
[weak self] in
self?.didFinishWriting?()
self?.didFinishWriting = nil
}
}
}
}
deinit {
didFinishWriting = nil
print("FileOutput deinited")
}
}
Related
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 ?? "")
}
}
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)
I have a multi-selection imagepicker with the intention of allowing the user to select multiple assets, then upload each asset to the database. In the completion handler, I take all the selected assets and pass them to a custom function: uploadImageAssets(assets: [PHAsset], projectRef: DocumentReference), where the upload begins.
In the function, I'm using a for-loop to upload each asset individually. While the assets are being uploaded correctly, not all assets are being uploaded. Lets say I've selected 5 assets... Only 4 will show up in the database, and they'll all be the same image, repeated. Any idea as to why this is happening? Here is my code below:
Image Picker Selection:
#IBAction func uploadProjectTapped(_ sender: Any) {
let imagePicker = ImagePickerController()
imagePicker.settings.selection.max = 10
imagePicker.settings.theme.selectionStyle = .numbered
imagePicker.settings.fetch.assets.supportedMediaTypes = [.image, .video]
imagePicker.settings.selection.unselectOnReachingMax = false
let start = Date()
self.presentImagePicker(imagePicker, select: { (asset) in
print("Selected: \(asset)")
}, deselect: { (asset) in
print("Deselected: \(asset)")
}, cancel: { (assets) in
print("Canceled with selections: \(assets)")
}, finish: { (assets) in
print("Finished with selections: \(assets)")
self.getAssetThumbnail(assets: assets)
}, completion: {
let finish = Date()
print(finish.timeIntervalSince(start))
})
}
And, the function to add them to Firestore:
func uploadImageAsset(assets: [PHAsset], projectRef: DocumentReference) {
let userID = Auth.auth().currentUser?.uid
let db = Firestore.firestore()
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
option.isSynchronous = false
option.isNetworkAccessAllowed = true
option.resizeMode = .exact
option.version = .original
option.deliveryMode = .highQualityFormat
let uniqueImageID = NSUUID().uuidString
let storageRef = Storage.storage().reference().child("project-images").child("\(uniqueImageID).jpeg")
for asset in assets {
let imageSize = CGSize(width: asset.pixelWidth, height: asset.pixelHeight)
manager.requestImage(for: asset, targetSize: imageSize, contentMode: .aspectFill, options: option) { (image, info) in
let uploadData = image?.jpegData(compressionQuality: 0.6)
storageRef.putData(uploadData!, metadata: nil, completion: {
(metadata, error) in
if error != nil {
return
} else {
storageRef.getMetadata(completion: { (metadata, err) in
if let error = err {
print(error)
} else {
storageRef.downloadURL(completion: { (url, err) in
if let error = err {
print(error)
} else {
self.imageAssetURLs.append((url?.absoluteString)!)
guard let url = url?.absoluteString else { return }
projectRef.updateData(["images": FieldValue.arrayUnion([url])], completion: { (err) in
if err != nil {
print(err)
} else {
self.dismiss(animated: true, completion: nil)
}
})
}
})
}
})
}
})
}
}
}
I have a strong feeling that the error lies within this line:
self.imageAssetURLs.append((url?.absoluteString)!)
guard let url = url?.absoluteString else { return }
in func uploadImageAsset(...) the
let uniqueImageID = NSUUID().uuidString
and
let storageRef = Storage.storage().reference().child("project-images").child("\(uniqueImageID).jpeg")
should be inside the loop just before
storageRef.putData(..)
I have 2 functions saveImage and loadImage:
//saveImage
func saveImage(imageName: String, image: UIImage) {
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let fileName = imageName
let fileURL = documentsDirectory.appendingPathComponent(fileName)
guard let data = image.jpegData(compressionQuality: 1) else { return }
if FileManager.default.fileExists(atPath: fileURL.path) {
do {
try FileManager.default.removeItem(atPath: fileURL.path)
print("Removed old image")
} catch let removeError {
print("couldn't remove file at path", removeError)
}
}
do {
try data.write(to: fileURL)
} catch let error {
print("error saving file with error", error)
}
}
//loadImage
func loadImageFromDocuments(fileName: String) -> UIImage? {
let documentDirectory = FileManager.SearchPathDirectory.documentDirectory
let userDomainMask = FileManager.SearchPathDomainMask.userDomainMask
let paths = NSSearchPathForDirectoriesInDomains(documentDirectory, userDomainMask, true)
if let dirPath = paths.first {
let imageUrl = URL(fileURLWithPath: dirPath).appendingPathComponent(fileName)
let image = UIImage(contentsOfFile: imageUrl.path)
return image
}
return nil
}
}
When I call in tableviewcelll like this:
self.cachedImageView.saveImage(imageName:,image:)
self.cachedImageView.loadImageFromDocuments(fileName:)
I don't how know use that.
Create image loader class like below :-
class PKImageLoader {
let imageCache = NSCache<NSString, UIImage>()
class var sharedLoader: PKImageLoader {
struct Static {
static let instance: PKImageLoader = PKImageLoader()
}
return Static.instance
}
func imageForUrl(urlPath: String, completionHandler: #escaping (_ image: UIImage?, _ url: String) -> ()) {
guard let url = urlPath.toUrl else {
return
}
if let image = imageCache.object(forKey: urlPath as NSString) {
completionHandler(image, urlPath)
}
else {
URLSession.shared.dataTask(with: url) { data, _, _ in
guard let finalData = data else { return }
DispatchQueue.main.async {
if let img = UIImage(data: finalData) {
self.imageCache.setObject(img, forKey: urlPath as NSString)
completionHandler(img, urlPath)
}
}
}.resume()
}
}
}
download an image from URL and save it (by creating UIImage Array)
you can use that array as you want.
use below extension for setting image directly to image view.
extension UIImageView {
func setImage(from urlPath: String, placeHolder: UIImage? = nil) {
self.image = placeHolder
PKImageLoader.sharedLoader.imageForUrl(urlPath: urlPath) { image, _ in
self.image = image
}
}
this one also help you
extension String {
var toUrl: URL? {
if self.hasPrefix("https://") || self.hasPrefix("http://") {
return URL(string: self)
}
else {
return URL(fileURLWithPath: self)
}
}
Audio file will not play after reducing it using AVAssetReader/ AVAssetWriter
At the moment, the whole function is being executed fine, with no errors thrown.
For some reason, when I go inside the document directory of the simulator via terminal, the audio file will not play through iTunes and comes up with error when trying to open with quicktime "QuickTime Player can't open "test1.m4a"
Does anyone specialise in this area and understand why this isn't working?
protocol FileConverterDelegate {
func fileConversionCompleted()
}
class WKAudioTools: NSObject {
var delegate: FileConverterDelegate?
var url: URL?
var assetReader: AVAssetReader?
var assetWriter: AVAssetWriter?
func convertAudio() {
let documentDirectory = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let exportURL = documentDirectory.appendingPathComponent(Assets.soundName1).appendingPathExtension("m4a")
url = Bundle.main.url(forResource: Assets.soundName1, withExtension: Assets.mp3)
guard let assetURL = url else { return }
let asset = AVAsset(url: assetURL)
//reader
do {
assetReader = try AVAssetReader(asset: asset)
} catch let error {
print("Error with reading >> \(error.localizedDescription)")
}
let assetReaderOutput = AVAssetReaderAudioMixOutput(audioTracks: asset.tracks, audioSettings: nil)
//let assetReaderOutput = AVAssetReaderTrackOutput(track: track!, outputSettings: nil)
guard let assetReader = assetReader else {
print("reader is nil")
return
}
if assetReader.canAdd(assetReaderOutput) == false {
print("Can't add output to the reader ☹️")
return
}
assetReader.add(assetReaderOutput)
// writer
do {
assetWriter = try AVAssetWriter(outputURL: exportURL, fileType: .m4a)
} catch let error {
print("Error with writing >> \(error.localizedDescription)")
}
var channelLayout = AudioChannelLayout()
memset(&channelLayout, 0, MemoryLayout.size(ofValue: channelLayout))
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo
// use different values to affect the downsampling/compression
let outputSettings: [String: Any] = [AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: 44100.0,
AVNumberOfChannelsKey: 2,
AVEncoderBitRateKey: 128000,
AVChannelLayoutKey: NSData(bytes: &channelLayout, length: MemoryLayout.size(ofValue: channelLayout))]
let assetWriterInput = AVAssetWriterInput(mediaType: .audio, outputSettings: outputSettings)
guard let assetWriter = assetWriter else { return }
if assetWriter.canAdd(assetWriterInput) == false {
print("Can't add asset writer input ☹️")
return
}
assetWriter.add(assetWriterInput)
assetWriterInput.expectsMediaDataInRealTime = false
// MARK: - File conversion
assetWriter.startWriting()
assetReader.startReading()
let audioTrack = asset.tracks[0]
let startTime = CMTime(seconds: 0, preferredTimescale: audioTrack.naturalTimeScale)
assetWriter.startSession(atSourceTime: startTime)
// We need to do this on another thread, so let's set up a dispatch group...
var convertedByteCount = 0
let dispatchGroup = DispatchGroup()
let mediaInputQueue = DispatchQueue(label: "mediaInputQueue")
//... and go
dispatchGroup.enter()
assetWriterInput.requestMediaDataWhenReady(on: mediaInputQueue) {
while assetWriterInput.isReadyForMoreMediaData {
let nextBuffer = assetReaderOutput.copyNextSampleBuffer()
if nextBuffer != nil {
assetWriterInput.append(nextBuffer!) // FIXME: Handle this safely
convertedByteCount += CMSampleBufferGetTotalSampleSize(nextBuffer!)
} else {
// done!
assetWriterInput.markAsFinished()
assetReader.cancelReading()
dispatchGroup.leave()
DispatchQueue.main.async {
// Notify delegate that conversion is complete
self.delegate?.fileConversionCompleted()
print("Process complete 🎧")
if assetWriter.status == .failed {
print("Writing asset failed ☹️ Error: ", assetWriter.error)
}
}
break
}
}
}
}
}
You need to call finishWriting on your AVAssetWriter to get the output completely written:
assetWriter.finishWriting {
DispatchQueue.main.async {
// Notify delegate that conversion is complete
self.delegate?.fileConversionCompleted()
print("Process complete 🎧")
if assetWriter.status == .failed {
print("Writing asset failed ☹️ Error: ", assetWriter.error)
}
}
}
If exportURL exists before you start the conversion, you should remove it, otherwise the conversion will fail:
try! FileManager.default.removeItem(at: exportURL)
As #matt points out, why the buffer stuff when you could do the conversion more simply with an AVAssetExportSession, and also why convert one of your own assets when you could distribute it already in the desired format?
The below code unzips a folder of images into a folder I create. and then loops through it and adds the names to an array, then loops through this array and retrieves those file names into an array of images.
It works perfectly on the simulator, but the data is empty when I do it on the iPad, it prints out nothing. I can only assume the folder isn't accessible or is being searched before the unzip has completed, but It shouldnt as I am using NSOperationQueue with a completion block.
func unzipData(objectData: NSManagedObject) {
var paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
let documentsDir = paths[0]
let zipPath = documentsDir.stringByAppendingString("MyZipFiles")
let folderPath = documentsDir.stringByAppendingString("/docLibFiles") // My folder name in document directory
var optData = NSData(data: objectData.valueForKey("image") as! NSData)
print(objectData.valueForKey("imageUrl") as! String)
optData.writeToFile(zipPath, atomically: true)
let success = fileManager.fileExistsAtPath(zipPath) as Bool
if success == false {
do {
try! fileManager.createDirectoryAtPath(folderPath, withIntermediateDirectories: true, attributes: nil)
}
}
queue.addOperationWithBlock { () -> Void in
let operation1 = NSBlockOperation(block: {
let unZipped = SSZipArchive.unzipFileAtPath(zipPath, toDestination: folderPath)
})
operation1.completionBlock = {
dispatch_async(dispatch_get_main_queue(), {
if queue.operationCount == 0 {
self.retrieveFiles()
}
})
}
queue.addOperation(operation1)
}
}
func getDocumentsURL() -> NSURL {
let documentsURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
return documentsURL
}
func fileInDocumentsDirectory(filename: String) -> String {
let fileURL = getDocumentsURL().URLByAppendingPathComponent(filename)
return fileURL.path!
}
func retrieveFiles() {
var paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
let documentsDir = paths[0]
let zipPath = documentsDir.stringByAppendingString("MyZipFiles")
let folderPath = documentsDir.stringByAppendingString("/docLibFiles") // My folder name in document directory
do {
let filelist = try fileManager.contentsOfDirectoryAtPath(folderPath)
print(filelist)
print("filename")
for filename in filelist {
fileNameArray.append(filename)
}
} catch let error as NSError {
print("Could not save \(error)")
}
do {
for item in fileNameArray {
print("item \(item)")
let imagePath = fileInDocumentsDirectory("docLibFiles/\(item)")
imageArray.append(UIImage(contentsOfFile: imagePath)!)
}
print("filename array \(fileNameArray)")
print("image array \(imageArray)")
unzipDelegate!.unzipSet(imageArray)
}
}
In case of avoiding an error is generated by some trivial typos. Probably process paths in NSURL form
let paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
let documentsDir = paths[0]
let url = NSURL(fileURLWithPath: documentsDir)
url.URLByAppendingPathComponent("MyZipFiles")
url.URLByAppendingPathComponent("docLibFiles")
Im not exactly sure why the initial problem was there, but my code was definitely overcomplicated. Ive sorted it now, hopefully it will help someone else in swift 2.1
class UnzipDocument {
let queue = NSOperationQueue() // BACKGROUND THREAD
var imageArray = [UIImage]()
var fileNameArray = [String]()
var fileManager = NSFileManager.defaultManager()
let compressedFile = NSTemporaryDirectory().stringByAppendingString("MyZipFiles")
let uncompressedFolder = NSTemporaryDirectory().stringByAppendingString("MyUnzippedFiles")
func unzipData(objectData: NSManagedObject){ // THIS IS BASICALLY NSDATA FROM CORE DATA FOR ME
let optData = NSData(data: objectData.valueForKey("image") as! NSData)
let success = fileManager.fileExistsAtPath(uncompressedFolder) as Bool // CREATES THE FOLDER IF IT DOESNT EXIST
if success == false {
do {
try! fileManager.createDirectoryAtPath(uncompressedFolder, withIntermediateDirectories: true, attributes: nil)
}
}
optData.writeToFile(compressedFile, atomically: true)
queue.addOperationWithBlock { () -> Void in
let operation1 = NSBlockOperation(block: {
SSZipArchive.unzipFileAtPath(self.compressedFile, toDestination: self.uncompressedFolder)
})
operation1.completionBlock = {
if queue.operationCount == 0 {
dispatch_async(dispatch_get_main_queue(), {
if queue.operationCount == 0 {
self.retrieveFiles()
}
})
}
}
queue.addOperation(operation1)
}
}
func retrieveFiles() {
do {
let filelist = try fileManager.contentsOfDirectoryAtPath(uncompressedFolder)
print(filelist)
print("filename")
for filename in filelist {
self.fileNameArray.append(filename)
}
} catch let error as NSError {
print("Could not save \(error)")
}
do {
for item in fileNameArray {
print("item \(item)")
let imagePath = uncompressedFolder.stringByAppendingString("/\(item)")
imageArray.append(UIImage(contentsOfFile: imagePath)!)
}
print("filename array \(fileNameArray)")
print("image array \(imageArray)")
unzipDelegate!.unzipSet(imageArray)
} catch {
}
}
}
and to delete the temp file/folder after use, so that it can be reused without any old documents remaining
var fileManager = NSFileManager.defaultManager()
let compressedFile = NSTemporaryDirectory().stringByAppendingString("MyZipFiles")
let uncompressedFolder = NSTemporaryDirectory().stringByAppendingString("MyUnzippedFiles")
do {
try fileManager.removeItemAtPath(compressedFile)
try fileManager.removeItemAtPath(uncompressedFolder)
} catch let error as NSError {
print("Could not save \(error)")
}