I am trying to save a large video locally to the photo library using PHPhotoLibrary but i notice that it takes a very long time is there any way to get progress or even better to make the process faster
my code:
func saveToLibrary(videoURL: URL, complition: #escaping () -> Void) {
PHPhotoLibrary.requestAuthorization { status in
guard status == .authorized else { return }
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
}) { success, error in
if !success {
print("Could not save video to photo library: \( error as Any)")
} else {
complition()
}
}
}
}
Do this on a background thread so that your UI doesn't get locked up.
You can first download the video in a temporary local file using NSURLSessionDownloadTask, and then pass the the URL from the that local file to PHPhotoLibrary. This way you can monitor the download progress.
Something like this would work:
// Download the remote URL to a file
let task = URLSession.shared.downloadTask(with: url) {
(tempURL, response, error) in
// Early exit on error
guard let tempURL = tempURL else {
return
}
// The file is downloaded in the tempURL we can save it in the library
saveToLibrary(videoURL: tempURL, complition: {})
}
// Start the download
task.resume()
// Monitor the progress
let observation = task.progress.observe(\.fractionCompleted) { progress, _ in
print(progress.fractionCompleted)
}
Related
I have an audio messaging app where I'd like a user to play an audio message and have it transcribed as it is playing.
Audio messages are recorded and stored via Firebase storage as m4a file types.
The error I am receiving is...
There was an error: Error Domain=AVFoundationErrorDomain Code=-11838 "Cannot initialize an instance of AVAssetReader with an asset at non-local URL
The following is my code...
func transcribeAudio(url: URL) {
let recognizer = SFSpeechRecognizer()
let request = SFSpeechURLRecognitionRequest(url: url)
recognizer?.recognitionTask(with: request) { [unowned self] (result, error) in
guard let result = result else {
print("There was an error: \(error!)")
DispatchQueue.main.async {
self.transcriptionLabel.text = "We're not able to transcribe this audio message."
}
return
}
if result.isFinal {
DispatchQueue.main.async {
self.transcriptionLabel.text = result.bestTranscription.formattedString
}
}
}
}
Any guidance would be appreciated. Have not been able to find any existing solution..
UPDATE:
I was able to get the transcription to work, but to make that possible...I had to download the URL I had fetched from Firebase storage via URLSession.shared.downloadTask(with: url), save it locally, and insert that local url into the SFSpeechURLRecognitionRequest method.
While this technically works, I'm really hoping there's a way I can just do it with the original non local url..it's also extremely slow.
My app as the functionality of choosing multiple images from the app main screen, and save the selected images to the user gallery.
As an example (image from google):
After the user clicking "save" I am doing the following in order to save the chosen images to the user's gallery.
Running through all of the images and saving each image that on clicked.
func saveSelectedImagesToDevice() {
for imageList in imagesListCells {
for image in imageList.images {
if image.selectionState == .onClicked {
downloadImage(from: image.url)
}
}
}
}
Downloading each image
func downloadImage(from url: String) {
guard let url = URL(string: url) else {return}
getData(from: url) { data, response, error in
guard let data = data, error == nil else { return }
guard let image = UIImage(data: data) else {return}
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
}
}
private func getData(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
#objc func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let _ = error {
self.delegate?.savedImage(proccessMsg: "Error adding images to the gallery, pleast make sure you enabled photos permissions in phone setting")
}
}
The thing is, because the saving process is asynchronies, in case error occurs in one of the process of downloading an image, I would like to stop all of the other asynchronies processes that running on the background.
At the moment, in case of error, the error been called for each one of the images.
Any ideas how can I manage it different in order to keep the process asynchronies but to be able to stop all processes in case of error?
You would have to change completely the architecture of the download to make it cancellable. A data task is cancellable, but yours is not because you have not retained any way of referencing it.
Apple suggests to not using the shared instance if you want to create multiple sessions. You could try to achieve this by creating a single session instance and invalidate it as soon as you receive an error.
Keep in mind that if you want to re-start the session you need to re instantiate a new one.
e.g.
let session = URLSession(configuration: .default)
func downloadImage(from url: String) {
guard let url = URL(string: url) else {return}
session.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
if let error = error {
print("You have an error: ",error.localizedDescription)
self.session.invalidateAndCancel()
}else if let data = data,
let image = UIImage(data: data) {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
}
}.resume()
}
In iOS 14 Apple has introduced PHPickerViewController where user has access to provide permission to all photo library videos or selected videos.
In first case when we provide permission to all videos,
we are able to get videos from photo library and able to convert it into the video-data to send it to backend server.
But in second case when user provide permission to selected videos,
In this scenario we are able to get the videos from the photo library ,but unable to convert it into the data from local video url.At that time data is always getting nil.
We have used below code to retrieve video from photo library url and converted it into the data.
// MARK: PHPickerViewControllerDelegate Methods
extension PhotoPickerVC: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
// Always dismiss the picker first
dismiss(animated: true)
if !results.isEmpty {
guard let itemProvider = results.first?.itemProvider else { return }
itemProvider.loadItem(forTypeIdentifier: "public.movie", options: nil) { [weak self] (fileURL, _) in
DispatchQueue.main.async {
guard let videoURL = fileURL as? URL, let _ = self else { return }
do {
//mediaURL video loading
print(videoURL)
let VideoData = try Data(contentsOf: videoURL, options: Data.ReadingOptions.alwaysMapped)
print(VideoData)
} catch _ {
print("Received nil VideoData")
}
}
}
}
}
}
This line is wrong:
itemProvider.loadItem(forTypeIdentifier: "public.movie", options: nil) { [weak self] (fileURL, _) in
You should not be trying to load any item. You can't hold a video in memory! You should be asking the provider to save the data to disk. I use this sort of code:
let movie = UTType.movie.identifier
itemProvider.loadFileRepresentation(forTypeIdentifier: movie) { url, err in
Note that you must immediately retrieve the URL, because this is a temporary location and the file will be deleted. If you want to preserve the file on disk, you must copy it off synchronously (on a background thead) to somewhere else.
Similarly, do not read the data from the file directly into memory. You can play the video from disk once you have preserved it; that's what it is for.
You can use like this type of
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
guard let provider = results.first?.itemProvider else { return }
provider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, err in
if let url = url {
print("video url \(url)")
}
}
if provider.canLoadObject(ofClass: UIImage.self) {
provider.loadObject(ofClass: UIImage.self) { image, _ in
self.parent.image = image as? UIImage
}
}
}
I'm uploading 2 files to the storage, once both are done a Firestore document should be created. This works perfectly if the app is in the foreground.
However if the app is put into the background while still uploading, the .success observer code does not get executed, the .progress observer still executes until it's 100% and then everything just stops.
Once I put my app back into the foreground the code gets executed again (sometimes successfully, sometimes with an error telling me to check the server response).
Button that calls the upload functions:
#IBAction func postButtonClicked(_ sender: Any) {
print("Post button clicked, trying to post...")
//Make sure we are logged in and have a video
guard let videoURL = videoURL else {
print("We don't have a video, why are we trying to post?")
dismiss(animated: true, completion: nil)
return
}
//1 Create and start uploading thumbnail
createThumbnail(from: videoURL) { (image) in
if let image = image {
self.uploadThumbnailToStorage(image: image)
}
}
//2 Start uploading video
uploadVideoToStorage(from: videoURL)
dismiss(animated: true, completion: nil)
}
And the video upload function:
func uploadVideoToStorage(from url: URL) {
print("Start uploading video...")
let storageRef = storage.reference()
let videoRef = storageRef.child("videos/\(postRef.documentID)_video")
// Upload the file to the path "videos"
let uploadTask = videoRef.putFile(from: url, metadata: nil) { metadata, error in
guard let metadata = metadata else {
// Uh-oh, an error occurred!
return
}
// Metadata contains file metadata such as size, content-type.
let size = metadata.size
print("Video file size: \(size)")
}
//Observe uploadTask
uploadTask.observe(.success) { snapshot in
snapshot.reference.downloadURL { (url, error) in
guard let downloadURL = url else {
if let error = error {
print("Error in video success observer: \(error.localizedDescription)")
}
return
}
print("Video upload success.")
self.videoString = downloadURL.absoluteString
self.videoUploadComplete = true
self.checkIfUploadsAreComplete()
}
}
uploadTask.observe(.progress) { snapshot in
let percentComplete = 100.0 * Double(snapshot.progress!.completedUnitCount)
/ Double(snapshot.progress!.totalUnitCount)
print("Uploading video: \(percentComplete.rounded())%")
}
}
The checkIfUploadsAreComplete() function checks if both upload tasks are complete and then writes the document, but I don't think that code is relevant.
I have tried putting the uploadVideoToStorage() function into the background like so:
DispatchQueue.global(qos: .background).async {
// Request the task assertion and save the ID.
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "TestVideoUpload") {
// End the task if time expires.
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
}
// Send the data synchronously.
DispatchQueue.main.async {
self.uploadVideoToStorage(from: videoURL)
}
// End the task assertion.
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
}
But that didn't do anything, any thoughts?
I am relatively new to iOS development and Swift but I have an app I'm working on which is supposed to record the activity on the screen and save the resulting video to the camera roll. I am using ReplayKit.
What is working now:
This is the code I have beginning the recording and ending the recording
the startRecording() function is run by a button that says "start" and the stopRecording() function is called by a button that says "stop".
var preview : RPPreviewViewController?
func startRecording() {
let recorder = RPScreenRecorder.sharedRecorder()
recorder.startRecordingWithMicrophoneEnabled(true) {
[unowned self] (error) in
print(recorder)
if let unwrappedError = error {
print(unwrappedError.localizedDescription)
}
}
}
func stopRecording() {
let recorder = RPScreenRecorder.sharedRecorder()
recorder.stopRecordingWithHandler {
[unowned self] (preview, error) in
if let unwrappedError = error {
print(unwrappedError.localizedDescription)
}
if let unwrappedPreview = preview {
print("end")
unwrappedPreview.previewControllerDelegate = self
unwrappedPreview.modalPresentationStyle=UIModalPresentationStyle.FullScreen
self.presentViewController(unwrappedPreview, animated: true, completion: nil)
}
}
The screen records fine. I have a button which says "Finish" which will call the stopRecording() function. When that button is clicked, a preview will show up which will play the recorded video and allow the user to manually edit and save the video.
What I'm trying to do:
I need to make the button simply save the video as is to the camera roll. I want to bypass the preview screen which allows the user to edit and manually save. Is this possible? If so, how would you approach the problem?
The preview is of type RPPreviewViewController? and try as I might, I just can't seem to access the video for saving. Since ReplayKit is an extension of UIKit, I tried using the
UISaveVideoAtPathToSavedPhotosAlbum(_ videoPath: String, _ completionTarget: AnyObject?, _ completionSelector: Selector, _ contextInfo: UnsafeMutablePointer<Void>)
method but none of those attributes exist!
If you need anymore info, please let me know. If I'm an idiot, please let me know! This is my first post here so be nice! and Thanks.
As mentioned by Geoff H, Replay Kit 2 now allows you to record the screen and save it either within your app or to the gallery without having to use the preview.
The documentation is sparse but after some trial and experiment the below code works in iOS 12.
Note this only captures video and not audio, although that should be straightforward to add, and you may want to add more error checking if using it. The functions below can be triggered by UI buttons, for example.
#objc func startRecording() {
//Use ReplayKit to record the screen
//Create the file path to write to
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
self.videoOutputURL = URL(fileURLWithPath: documentsPath.appendingPathComponent("MyVideo.mp4"))
//Check the file does not already exist by deleting it if it does
do {
try FileManager.default.removeItem(at: videoOutputURL)
} catch {}
do {
try videoWriter = AVAssetWriter(outputURL: videoOutputURL, fileType: AVFileType.mp4)
} catch let writerError as NSError {
os_log("Error opening video file", writerError);
videoWriter = nil;
return;
}
//Create the video settings
let videoSettings: [String : Any] = [
AVVideoCodecKey : AVVideoCodecType.h264,
AVVideoWidthKey : 1920, //Replace as you need
AVVideoHeightKey : 1080 //Replace as you need
]
//Create the asset writer input object whihc is actually used to write out the video
//with the video settings we have created
videoWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoSettings);
videoWriter.add(videoWriterInput);
//Tell the screen recorder to start capturing and to call the handler when it has a
//sample
RPScreenRecorder.shared().startCapture(handler: { (cmSampleBuffer, rpSampleType, error) in
guard error == nil else {
//Handle error
os_log("Error starting capture");
return;
}
switch rpSampleType {
case RPSampleBufferType.video:
os_log("writing sample....");
if self.videoWriter.status == AVAssetWriter.Status.unknown {
if (( self.videoWriter?.startWriting ) != nil) {
os_log("Starting writing");
self.videoWriter.startWriting()
self.videoWriter.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(cmSampleBuffer))
}
}
if self.videoWriter.status == AVAssetWriter.Status.writing {
if (self.videoWriterInput.isReadyForMoreMediaData == true) {
os_log("Writting a sample");
if self.videoWriterInput.append(cmSampleBuffer) == false {
print(" we have a problem writing video")
}
}
}
default:
os_log("not a video sample, so ignore");
}
} )
}
#objc func stoprecording() {
//Stop Recording the screen
RPScreenRecorder.shared().stopCapture( handler: { (error) in
os_log("stopping recording");
})
self.videoWriterInput.markAsFinished();
self.videoWriter.finishWriting {
os_log("finished writing video");
//Now save the video
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: self.videoOutputURL)
}) { saved, error in
if saved {
let alertController = UIAlertController(title: "Your video was successfully saved", message: nil, preferredStyle: .alert)
let defaultAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(defaultAction)
self.present(alertController, animated: true, completion: nil)
}
if error != nil {
os_log("Video did not save for some reason", error.debugDescription);
debugPrint(error?.localizedDescription ?? "error is nil");
}
}
}
I too wanted to do what you have asked, but as of now RPScreenRecorder doesn't provide any of those functionalities.
Yes, you can. Check this ReplayKit2 Swift 4:
https://medium.com/#giridharvc7/replaykit-screen-recording-8ee9a61dd762
Once you have the file, it shouldn't be too much trouble to save it to the camera roll with something along the lines of:
static func saveVideo(url: URL, returnCompletion: #escaping (String?) -> () ) {
DispatchQueue.global(qos: .userInitiated).async {
guard let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
if !FileManager.default.fileExists(atPath: documentsDirectoryURL.appendingPathComponent(url.lastPathComponent).path) {
URLSession.shared.downloadTask(with: url) { (location, response, error) -> Void in
guard let location = location else { return }
let destinationURL = documentsDirectoryURL.appendingPathComponent(response?.suggestedFilename ?? url.lastPathComponent)
do {
try FileManager.default.moveItem(at: location, to: destinationURL)
PHPhotoLibrary.requestAuthorization({ (authorizationStatus: PHAuthorizationStatus) -> Void in
if authorizationStatus == .authorized {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: destinationURL)}) { completed, error in
DispatchQueue.main.async {
if completed { returnCompletion(url.lastPathComponent)
} else {
returnCompletion(nil)
}
}
}
}
})
returnCompletion(url.lastPathComponent)
} catch {
returnCompletion(nil)
}
}.resume()
} else {
returnCompletion(nil)
}
}
}
I am running into an error, when it hits:
self.videoWriterInput.markAsFinished();
It is giving me :
-[AVAssetWriterInput markAsFinished] Cannot call method when status is 0