How to Save ReplayKit Video to Camera Roll with In-App Button - ios

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

Related

Saving large videos in iOS

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

How to run a function in a for loop sequentially

I have a set of buttons in a stack view. Each button when pressed plays a different sound. I have a separate button (loop button) that when pressed calls the loopButtonPressed function. My goal is that when this loop button is pressed, it will loop through the subviews that are buttons in this stack view and play each of the sounds sequentially in order using the soundButtonPressed function. I saw a method that I implemented below using the run() function which sets each consecutive function to run after a given amount of time. Although this kind of works it is not a great solution because the sound files are of varying length. I was thinking there may be a way to do this using dispatch groups, which I don't fully understand. If I take away the run function, it will only play the sound of the last button in the stack view. I am using AVFoundation to play the wav files as well. I appreciate any advice or direction, thanks.
func run(after seconds: Int, completion: #escaping () -> Void) {
let deadline = DispatchTime.now() + .milliseconds(seconds)
DispatchQueue.main.asyncAfter(deadline: deadline) {
completion()
}
}
#objc func loopButtonPressed(_ sender: UIButton) {
var i = 1
for case let button as UIButton in self.colorBubblesStackView.subviews {
run(after: 800*i) {
self.soundButtonPressed(sender: button)
}
i += 1
}
}
My soundButtonPressed function is just a switch statement where each case calls the function playSound() with the correct sound file name. Here is the playSound function:
func playSound(_ soundFileName: String) {
guard let url = Bundle.main.url(forResource: soundFileName, withExtension: "wav") else { return }
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.wav.rawValue)
guard let player = player else { return }
player.play()
} catch let error {
print(error.localizedDescription)
}
}
func playSound(name: String ) {
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else {
print("url not found")
return
}
do {
/// this codes for making this app ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
/// change fileTypeHint according to the type of your audio file (you can omit this)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
player?.delegate = self
// no need for prepareToPlay because prepareToPlay is happen automatically when calling play()
player!.play()
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
print("finished")//It is working now! printed "finished"!
}
confirm to protocol
class ViewController: UIViewController,AVAudioPlayerDelegate {
and instead of looping add each button a Tag and start from first tag sat 100 . Then when the call back obtained for player finished playing play the next File with new icremented Tag say 101
Try AVQueuePlayer
A player used to play a number of items in sequence.
#objc func loopButtonPressed(_ sender: UIButton) {
let allUrls = allSongs.compactMap { Bundle.main.url(forResource: $0, withExtension: "wav") }
let items = allUrls.map { AVPlayerItem(url: $0) }
let queuePlayer = AVQueuePlayer(items: items)
queuePlayer.play()
}

While I keep my wrist down, how to keep playing audio in iWatch app? - watchOS

I am trying to build an audio app for apple watch. But the problem is whenever I keep my hands down , audio will stop playing.
I have turned background mode on as well.
Can anyone please help me with this? I am stuck at this part.
Here is the Code I have used for playing audio.
func play(url : URL) {
do {
if #available(watchOSApplicationExtension 4.0, *) {
WKExtension.shared().isFrontmostTimeoutExtended = true
} else {
// Fallback on earlier versions
}
self.player = try AVAudioPlayer(contentsOf: url)
player!.prepareToPlay()
player?.delegate = self
player?.play()
print("-----------------")
print("Playing Audio")
print("*****************\nCurrent Time \(String(describing: self.player?.currentTime))")
} catch let error as NSError {
self.player = nil
print(error.localizedDescription)
} catch {
print("*************************")
print("AVAudioPlayer init failed")
}
}
Make sure you are trying to play with Audio Data, not Audio URL and have added policy: .longFormAudio in your category setup. As per Apple documentation, these two settings have to be set for audio to play in background mode.
// Set up the session.
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(
.playback,
mode: .default,
policy: .longFormAudio
)
} catch let error {
fatalError("*** Unable to set up the audio session: \(error.localizedDescription) ***")
}
// Set up the player.
let player: AVAudioPlayer
do {
player = try AVAudioPlayer(data: audioData)
} catch let error {
print("*** Unable to set up the audio player: \(error.localizedDescription) ***")
// Handle the error here.
return
}
// Activate and request the route.
session.activate(options: []) { (success, error) in
guard error == nil else {
print("*** An error occurred: \(error!.localizedDescription) ***")
// Handle the error here.
return
}
// Play the audio file.
player.play()
}
I have tested this code and its working with only Bluetooth connectivity in Watch application not in watch speaker.
Simply turning on background mode is not enough. You also need to activate the AVAudioSession.
It's all well documented by Apple here: Playing Background Audio.
Configure and Activate the Audio Session
Before you can play audio, you need to set up and activate the audio session.
session.setCategory(AVAudioSession.Category.playback,
mode: .default,
policy: .longForm,
options: [])
Next, activate the session, by calling the activate(options:completionHandler:) method.
session.activate(options: []) { (success, error) in
// Check for an error and play audio.
}
Ref: https://developer.apple.com/documentation/watchkit/playing_background_audio
Example:
var player: AVAudioPlayer?
let session: AVAudioSession = .sharedInstance()
func prepareSession() {
do {
try session.setCategory(AVAudioSession.Category.playback,
mode: .default,
policy: .longForm,
options: [])
}
catch {
print(error)
}
}
func play(url: URL) {
do {
player = try AVAudioPlayer(contentsOf: url)
}
catch {
print(error)
return
}
session.activate(options: []) { (success, error) in
guard error == nil else {
print(error!)
return
}
// Play the audio file
self.player?.play()
}
}
Simple Test:
prepareSession()
if let url = Bundle.main.url(forResource: "test", withExtension: "mp3") {
play(url: url)
}
else {
print("test.mp3 not found in project: put any mp3 file in and name it so")
}

Firestore storage upload task observer not executing while app is in background

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?

Play Spotify playlist or album music in my iOS app

Error Domain=com.spotify.ios-sdk.playback Code=1 "The operation failed due to an unspecified issue." UserInfo={NSLocalizedDescription=The operation failed due to an unspecified issue
Here is my code
func getAlbums(authSession:SPTSession){
SPTPlaylistList.playlists(forUser: authSession.canonicalUsername, withAccessToken: authSession.accessToken) { (error, response) in
if let listPage = response as? SPTPlaylistList, let albums = listPage.items as? [SPTPartialPlaylist]{
for item in albums{
let stringFromUrl = item.uri
// use SPTPlaylistSnapshot to get all the playlists
print("************************\(item.name)************************")
SPTPlaylistSnapshot.playlist(withURI: stringFromUrl, accessToken: authSession.accessToken!) { (error, snap) in
if let s = snap as? SPTPlaylistSnapshot {
print("====================\(s.name)=====================")
// get the tracks for each playlist
SpotifyManager.MyAlbums.append(s)
if SpotifyManager.MyAlbums.count == albums.count{
NotificationCenter.default.post(name: NSNotification.Name(rawValue: relodNotification), object: nil)
}
}
}
}
}
}
}
This error comes from this method
#objc func play() {
print(songInfo.uri)
SpotifyManager.player?.playSpotifyURI(self.songInfo.uri.absoluteString, startingWith: 0, startingWithPosition: 0, callback: { (err) in
print(err.debugDescription)
})
}
if anybody face this type of problem please give me the best solution to play the Spotify music in my app.
Just remove the looping because the player does that automatically for you!

Resources