Record screen and save video to server in Swift - ios

I have a code that uses ReplayKit to record the screen, and saves it in the documents Directory of the app. The problem comes when I am trying to access it, it does not find the URL of the video.
import SwiftUI
import ReplayKit
import AVKit
struct ContentView: View {
#State var screenRecording = ScreenRecording.singleton
#State var buttonMessage: String = "Start recording"
#State var showVideo: Bool = false
var body: some View {
Button(buttonMessage) {
if screenRecording.recording {
screenRecording.stopRecording()
buttonMessage = "Start recording"
} else {
screenRecording.startRecording()
buttonMessage = "Stop recording"
}
screenRecording.recording = !screenRecording.recording
}
Button("next screen") {
showVideo = true
}
if showVideo {
VideoPlayer(player: AVPlayer(url: screenRecording.getDocument()))
.frame(height: 400)
.onAppear {
print("URL: \(screenRecording.getDocument().path)")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class ScreenRecording: ObservableObject {
var recording: Bool = false
private let screenRecorder = RPScreenRecorder.shared()
static let singleton = ScreenRecording()
private init() {}
func startRecording() {
if !screenRecorder.isRecording && RPScreenRecorder.shared().isAvailable {
screenRecorder.isMicrophoneEnabled = true
screenRecorder.isCameraEnabled = true
screenRecorder.startRecording { err in
guard err == nil else {
print("Failed to start recording...")
return
}
print("Recording starts!")
}
} else {
print("Trying to start recording when a recording is already happening!")
}
}
func stopRecording() {
if screenRecorder.isRecording {
screenRecorder.stopRecording(withOutput: getDocumentDirectoryURL()) { err in
guard err == nil else {
print("An error has ocurred: \(String(describing: err))")
return
}
}
print("Recording stops!")
} else {
print("Trying to stop recording when NO recording is initiated!")
}
}
func getDocumentDirectoryURL() -> URL {
let documentDirURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
print("Filepath is: \(documentDirURL.path)")
return documentDirURL
}
func getDocument() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
print("This re the paths: \(paths)")
return paths[0]
}
}
The method getDocument should return the first URL of the directory, which should be the video file, but it does not work. Where is the problem?

You can check Apple's demo
Recording and Streaming Your macOS App
The relevant code is
func exportClip() {
let clipURL = getDirectory()
let interval = TimeInterval(5)
print("Generating clip at URL: ", clipURL)
RPScreenRecorder.shared().exportClip(to: clipURL, duration: interval) { error in
if error != nil {
print("Error attempting to start Clip Buffering")
} else {
// There isn't an error, so save the clip at the URL to Photos.
self.saveToPhotos(tempURL: clipURL)
}
}
}
func getDirectory() -> URL {
var tempPath = URL(fileURLWithPath: NSTemporaryDirectory())
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd-hh-mm-ss"
let stringDate = formatter.string(from: Date())
print(stringDate)
tempPath.appendPathComponent(String.localizedStringWithFormat("output-%#.mp4", stringDate))
return tempPath
}
now use stopRecording(withOutput: , instead of exportClip(to:
And more:
according to apple's doc
stopRecording(withOutput:completionHandler:)
the movie is written to the specified output URL.
we need the mp4 file

Related

Convert PDF To Base64 in SwiftUI

I am fetching PDF data. It provides me URL, and when I am converting it to base64. This is what I am getting:
The file “file.pdf” couldn’t be opened because you don’t have permission to view it.
I am not getting why is it giving this. This is how I am getting the pdf file url:
#State var openPDF = false
#State var fileName = ""
#State var pdfFile : URL?
var body: some View {
Button {
self.openPDF.toggle()
}, label {
Text("Add Attachment")
}.fileImporter(isPresented: $vm.openPDF, allowedContentTypes: [.pdf]) { result in
do {
let fileURL = try result.get()
print(fileURL)
self.fileName = fileURL.lastPathComponent
self.pdfFile = fileURL
} catch {
print("error getting documents")
print(error.localizedDescription)
}
}
}
An this is how I am converting it to base64:
do {
let filePath = pdfFile
let fileData = try Data.init(contentsOf: filePath!)
let fileStream:String = fileData.base64EncodedString()
print(fileStream)
} catch {
print("error")
print(error.localizedDescription)
}
In this do-catch, it is giving the error that "this file could not be opened because you don't have permission.
Do this to convert PDF to Base64 String:
fileapp.startAccessingSecurityScopedResource() {
defer {
DispatchQueue.main.async {
fileapp.stopAccessingSecurityScopedResource()
}
}
do {
let fileData = try Data.init(contentsOf: fileapp)
let fileStream:String = fileData.base64EncodedString()
print(fileStream)
base64PDF = fileStream
} catch {
print("error")
print(error.localizedDescription)
}
}

Can't change frame size of LPLinkView in a SwiftUI List

I am trying to display rich links in a SwiftUI List and no matter what I try, I can't seem to be able to change the size of the link view (UIViewRepresentable) on screen.
Is there a minimum size for a particular link? And how can I get it. Adding .aspectRatio and clipped() will respect size but the link is heavily clipped. Not sure why the link will not adjust aspectRatio to fit view.
Some of the following code is sourced from the following tutorial:
https://www.appcoda.com/linkpresentation-framework/
I am using the following UIViewRepresentable for the LinkView:
import SwiftUI
import LinkPresentation
struct LinkViewRepresentable: UIViewRepresentable {
typealias UIViewType = LPLinkView
var metadata: LPLinkMetadata?
func makeUIView(context: Context) -> LPLinkView {
guard let metadata = metadata else { return LPLinkView() }
let linkView = LPLinkView(metadata: metadata)
return linkView
}
func updateUIView(_ uiView: LPLinkView, context: Context) {
}
}
And my view with List is:
import SwiftUI
import LinkPresentation
struct ContentView: View {
#ObservedObject var linksViewModel = LinksViewModel()
var links: [(String, String)] = [("https://www.apple.com", "1"), ("https://www.stackoverflow.com", "2")]
var body: some View {
ScrollView(.vertical) {
LazyVStack {
ForEach(links, id: \.self.1) { link in
VStack {
Text(link.0)
.onAppear {
linksViewModel.getLinkMetadata(link: link)
}
if let richLink = linksViewModel.links.first(where: { $0.id == link.1 }) {
if let metadata = richLink.metadata {
if metadata.url != nil {
LinkViewRepresentable(metadata: metadata)
.frame(width: 200) // setting frame dimensions here has no effect
}
}
}
}
}
}
.padding()
}
}
}
Setting the frame of the view or contentMode(.fit) or padding or anything else I've tried does not change the size of the frame of the LinkViewRepresentable. I have tried sizeToFit in the representable on update and no luck. Is it possible to control the size of the representable view here?
Here are additional Files:
import Foundation
import LinkPresentation
class LinksViewModel: ObservableObject {
#Published var links = [Link]()
init() {
loadLinks()
}
func createLink(with metadata: LPLinkMetadata, id: String) {
let link = Link()
link.id = id
link.metadata = metadata
links.append(link)
saveLinks()
}
fileprivate func saveLinks() {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: links, requiringSecureCoding: true)
guard let docDirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
try data.write(to: docDirURL.appendingPathComponent("links"))
print(docDirURL.appendingPathComponent("links"))
} catch {
print(error.localizedDescription)
}
}
fileprivate func loadLinks() {
guard let docDirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let linksURL = docDirURL.appendingPathComponent("links")
if FileManager.default.fileExists(atPath: linksURL.path) {
do {
let data = try Data(contentsOf: linksURL)
guard let unarchived = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [Link] else { return }
links = unarchived
} catch {
print(error.localizedDescription)
}
}
}
func fetchMetadata(for link: String, completion: #escaping (Result<LPLinkMetadata, Error>) -> Void) {
guard let uRL = URL(string: link) else { return }
let metadataProvider = LPMetadataProvider()
metadataProvider.startFetchingMetadata(for: uRL) { (metadata, error) in
if let error = error {
print(error)
completion(.failure(error))
return
}
if let metadata = metadata {
completion(.success(metadata))
}
}
}
func getLinkMetadata(link: (String, String)) {
for storedLink in self.links {
if storedLink.id != link.1 {
return
}
}
do {
let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let matches = detector.matches(in: link.0, options: [], range: NSRange(location: 0, length: link.0.utf16.count))
if let match = matches.first {
guard let range = Range(match.range, in: link.0) else { return }
let uRLString = link.0[range]
self.fetchMetadata(for: String(uRLString)) { result in
self.handleLinkFetchResult(result, link: link)
}
}
} catch {
print(error)
}
}
private func handleLinkFetchResult(_ result: Result<LPLinkMetadata, Error>, link: (String, String)) {
DispatchQueue.main.async {
switch result {
case .success(let metadata):
self.createLink(with: metadata, id: link.1)
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
And Link Class:
import Foundation
import LinkPresentation
class Link: NSObject, NSSecureCoding, Identifiable {
var id: String?
var metadata: LPLinkMetadata?
override init() {
super.init()
}
// MARK: - NSSecureCoding Requirements
static var supportsSecureCoding = true
func encode(with coder: NSCoder) {
guard let id = id, let metadata = metadata else { return }
coder.encode(id, forKey: "id")
coder.encode(metadata as NSObject, forKey: "metadata")
}
required init?(coder: NSCoder) {
id = coder.decodeObject(forKey: "id") as? String
metadata = coder.decodeObject(of: LPLinkMetadata.self, forKey: "metadata")
}
}
This is what I get:
The solution that worked for me was subclassing the linkView overriding the intrinsic content size. Thanks to user1046037's comment, using super.intrinsicContentSize.height will enable it to work dynamically.
import SwiftUI
import LinkPresentation
class CustomLinkView: LPLinkView {
override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) }
}
struct LinkViewRepresentable: UIViewRepresentable {
typealias UIViewType = CustomLinkView
var metadata: LPLinkMetadata?
func makeUIView(context: Context) -> CustomLinkView {
guard let metadata = metadata else { return CustomLinkView() }
let linkView = CustomLinkView(metadata: metadata)
return linkView
}
func updateUIView(_ uiView: CustomLinkView, context: Context) {
}
}

URLSession for XMLParser in SwiftUI returning empty or nil

I am attempting to parse XML using an URLSession, XCode 12, SwiftUI but it keeps returning [] or nil. If I print immediately after the parse(see code), all the data is there, but for some reason, it seems to be clearing it all out.
If I try it with a .xml file, the code works fine, so it must be something in my URLSession in a XMLParserDelegate class:
class ParseController: NSObject, XMLParserDelegate{
var items: [Item] = []
var itemStore: [Item]?
func loadData() {
let url = URL(string: "website")!
let request=URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if data == nil {
print("dataTaskWithRequest error: \(String(describing: error?.localizedDescription))")
return
}
let parser = XMLParser(data: data!)
parser.delegate=self
parser.parse()
self.itemStore=self.items
print(self.itemStore)
}
task.resume()
/*
if let path = Bundle.main.url(forResource: "Items", withExtension: "xml") {
if let parser = XMLParser(contentsOf: path) {
parser.delegate = self
parser.parse()
self.itemStore=self.items
}
}
*/
}
And then I call that with a button in my View:
struct MyParserView: View {
#State var itemsResult: [Item]?
var body: some View {
if ((itemsResult?.isEmpty) == nil) {
VStack {
Text("Stuff here")
Button(action: {
let parserControl = ParseController()
parserControl.loadData()
itemsResult = parserControl.itemStore
}){
Text("Push")
}
}
}else {
List{
ForEach(itemResult!, id:\.id){item in
Text(item.name)
}
}
}
}
}
This was not a problem with XML, but lifecycle in SwiftUI.
https://developer.apple.com/forums/thread/662429
TL;DR: I published itemStore and now works great!

RPScreenRecorder.shared().isAvailable is always false

I'm trying to record my screen with a sample ios application.
But it does not work because RPScreen.shared().isAvailable always returns false.
These are my codes:
ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var StartRecordingButton: UIButton!
#IBOutlet weak var EndRecordingButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
StartRecordingButton.addTarget(self, action: #selector(startRecord(_:)), for: .touchUpInside)
EndRecordingButton.addTarget(self, action: #selector(stopRecord(_:)), for: .touchUpInside)
}
private lazy var recorder: ScreenRecorder = ScreenRecorder(configuration: ScreenRecorder.Configuration(), completion: {
(url, error) in
guard let url = url else {
fatalError("\(#function) record failed \(String(describing: error))")
}
debugPrint(#function, "success", url)
})
#objc func startRecord(_ sender: UIButton) {
recordStart()
}
#objc func stopRecord(_ sender: UIButton) {
recordStop()
}
private func recordStart() {
guard !recorder.isRecording else { return }
do {
try recorder.start()
} catch {
fatalError("start recording failed \(error)")
}
}
private func recordStop() {
guard recorder.isRecording else { return }
do {
try recorder.end()
} catch {
fatalError("finish recording failed \(error)")
}
}
}
ScreenRecorder.swift
import ReplayKit
#available(iOS 11.0, *)
public class ScreenRecorder: NSObject {
let screenRecorder = RPScreenRecorder.shared()
// Alias for arguments
public typealias Completion = (URL?, Error?) -> ()
let completion: Completion
let configuration: Configuration
public init (configuration: Configuration, completion: #escaping Completion) {
self.configuration = configuration
self.completion = completion
super.init()
}
// Start recording screen
public func start() throws {
print(screenRecorder.isAvailable)
guard screenRecorder.isAvailable else {
throw ScreenRecorderError.notAvailable
}
guard !screenRecorder.isRecording else {
throw ScreenRecorderError.alreadyRunning
}
try setUp()
assetWriter?.startWriting()
assetWriter?.startSession(atSourceTime: CMTime.zero)
screenRecorder.startCapture(handler: { [weak self] (cmSampleBuffer, rpSampleBufferType, error) in
if let error = error {
debugPrint(#function, "something happened", error)
}
if RPSampleBufferType.video == rpSampleBufferType {
self?.appendVideo(sampleBuffer: cmSampleBuffer)
}
}) { [weak self] (error) in
if let error = error {
self?.completion(nil, error)
}
}
}
public func end() throws {
guard screenRecorder.isRecording else {
throw ScreenRecorderError.notRunning
}
screenRecorder.stopCapture { [weak self] (error) in
if let error = error {
self?.completion(nil, error)
}
self?.videoAssetWriterInput?.markAsFinished()
self?.assetWriter?.finishWriting {
DispatchQueue.main.async {
self?.completion(self?.cacheFileURL, nil)
}
}
}
}
public var isRecording: Bool {
return screenRecorder.isRecording
}
private var startTime: CMTime?
private var assetWriter: AVAssetWriter?
private var videoAssetWriterInput: AVAssetWriterInput?
private var writerInputPixelBufferAdapter: AVAssetWriterInputPixelBufferAdaptor?
private func setUp() throws {
try createCacheDirectoryIfNeeded()
try removeOldCachedFile()
guard let cacheURL = cacheFileURL else {
throw ScreenRecorderError.invalidURL
}
let assetWriter = try AVAssetWriter(url: cacheURL, fileType: configuration.fileType)
let videoSettings: [String: Any] = [
AVVideoCodecKey: configuration.codec,
AVVideoWidthKey: UInt(configuration.videoSize.width),
AVVideoHeightKey: UInt(configuration.videoSize.height),
]
let videoAssetWriterInput = try AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
videoAssetWriterInput.expectsMediaDataInRealTime = true
if assetWriter.canAdd(videoAssetWriterInput) {
assetWriter.add(videoAssetWriterInput)
}
self.assetWriter = assetWriter
self.videoAssetWriterInput = videoAssetWriterInput
self.writerInputPixelBufferAdapter = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoAssetWriterInput, sourcePixelBufferAttributes: [
kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32ARGB)
])
}
private func appendVideo(sampleBuffer: CMSampleBuffer) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
let firstTime: CMTime
if let startTime = self.startTime {
firstTime = startTime
} else {
firstTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
startTime = firstTime
}
let currentTime: CMTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let diffTime: CMTime = CMTimeSubtract(currentTime, firstTime)
if writerInputPixelBufferAdapter?.assetWriterInput.isReadyForMoreMediaData ?? false {
writerInputPixelBufferAdapter?.append(pixelBuffer, withPresentationTime: diffTime)
}
}
private func createCacheDirectoryIfNeeded() throws {
guard let cacheDirectoryURL = cacheDirectoryURL else { return }
let fileManager = FileManager.default
guard !fileManager.fileExists(atPath: cacheDirectoryURL.path) else { return }
try fileManager.createDirectory(at: cacheDirectoryURL, withIntermediateDirectories: true, attributes: nil)
}
private func removeOldCachedFile() throws {
guard let cacheURL = cacheFileURL else { return }
let fileManager = FileManager.default
guard fileManager.fileExists(atPath: cacheURL.path) else { return }
try fileManager.removeItem(at: cacheURL)
}
private var cacheDirectoryURL: URL? = {
guard let path = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first else {
return nil
}
return URL(fileURLWithPath: path).appendingPathComponent("ScreenRecorder")
}()
private var cacheFileURL: URL? {
guard let cacheDirectoryURL = cacheDirectoryURL else { return nil }
return cacheDirectoryURL.appendingPathComponent("screenrecord.mp4")
}
}
#available(iOS 11.0, *)
extension ScreenRecorder {
public struct Configuration{
public var codec: AVVideoCodecType = .h264
public var fileType: AVFileType = .mp4
public var videoSize: CGSize = CGSize(
width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height
)
public var audioQuality: AVAudioQuality = .medium
public var audioFormatID: AudioFormatID = kAudioFormatMPEG4AAC
public var numberOfChannels: UInt = 2
public var sampleRate: Double = 44100.0
public var bitrate: UInt = 16
public init() {}
}
public enum ScreenRecorderError: Error {
case notAvailable
case alreadyRunning
case notRunning
case invalidURL
}
}
And it shows this fatal error which I wrote:
ios_record_screen[1258:213516] Fatal error: start recording failed notAvailable
I've enabled screen recording in Settings app in my iPhone8, and tried to run on my friend's iPhone X as well.
But both phones didn't work...
I could not find helpful information in the Internet.
Hope a help.
I hope the problem for those who struggled before has been resolved
In my case,
override func viewDidLoad()
needed
RPScreenRecorder.shared().delegate = self
syntax.
Of course, even the delegate extension that comes with it.
I was implementing RPScreenRecorder in a new view, which was working normally in other views, and I encountered the same problem as the author in the process.
It was a problem that the delegate was not imported while looking for a difference from the previous one.
Hope this helps anyone who finds this page in the future.

How can I Save Json Data and Show Offline in swift

I'm trying to save my json file and show it to offline. I'm trying this code but it is not working for me ..
let myData = NSKeyedArchiver.archivedData(withRootObject: self.data)
UserDefaults.standard.set(myData, forKey: "userJson")
UserDefaults.standard.synchronize()
Can any one suggest me better way to save data and show off line ?
You should not save JSON in the UserDefault, Instead save it in file in document directory
I have created generic class which allows to do it easily
//
// OfflineManager.swift
//
//
// Created by Prashant on 01/05/18.
// Copyright © 2018 Prashant. All rights reserved.
//
import UIKit
class OfflineManager: NSObject {
static let sharedManager = OfflineManager()
let LocalServiceCacheDownloadDir = "LocalData"
enum WSCacheKeys {
case CampignList
case CampignDetail(id:String)
case ScreenShotList
var value:String {
switch self {
case .CampignList:
return "CampignList"
case .CampignDetail(let id):
return id
case .ScreenShotList :
return "ScreenShotList"
}
}
}
func getBaseForCacheLocal(with fileName:String) -> String? {
let filePath = FileManager.default.getDocumentPath(forItemName: self.LocalServiceCacheDownloadDir)
if FileManager.default.directoryExists(atPath: filePath) {
return filePath.stringByAppendingPathComponent(fileName)
} else {
if FileManager.default.createDirectory(withFolderName: self.LocalServiceCacheDownloadDir) {
return filePath.stringByAppendingPathComponent(fileName)
}
}
return nil
}
//------------------------------------------------------------
#discardableResult
func cacheDataToLocal<T>(with Object:T,to key:WSCacheKeys) -> Bool {
let success = NSKeyedArchiver.archiveRootObject(Object, toFile: getBaseForCacheLocal(with: key.value)!)
if success {
print( "Local Data Cached\(String(describing: getBaseForCacheLocal(with: key.value)))")
} else {
print("Error")
}
return success
}
//------------------------------------------------------------
func loadCachedDataFromLocal<T>(with key:WSCacheKeys ) -> T? {
return NSKeyedUnarchiver.unarchiveObject(withFile: getBaseForCacheLocal(with: key.value)!) as? T
}
//------------------------------------------------------------
func removeAllCacheDirs () {
do {
try FileManager.default.removeItem(atPath: self.getBaseForCacheLocal(with: "")!)
} catch {
print("error in remove dir \(error.localizedDescription)")
}
}
//--------------------------------------------------------------------------------
}
Here is some helper methods of extension FileManager
public var getDocumentDirectoryPath: String {
let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
return documentDirectory
}
public func getDocumentPath(forItemName name: String)-> String {
return getDocumentDirectoryPath.stringByAppendingPathComponent(name)
}
public func directoryExists(atPath filePath: String)-> Bool {
var isDir = ObjCBool(true)
return FileManager.default.fileExists(atPath: filePath, isDirectory: &isDir )
}
public func createDirectory(withFolderName name: String)-> Bool {
let finalPath = getDocumentDirectoryPath.stringByAppendingPathComponent(name)
return createDirectory(atPath: finalPath)
}
Here Is String extension's method
public func stringByAppendingPathComponent(_ path: String) -> String {
let fileUrl = URL.init(fileURLWithPath: self)
let filePath = fileUrl.appendingPathComponent(path).path
return filePath
}
How to use it ?
To save
OfflineManager.sharedManager.cacheDataToLocal(with: object as! [String:Any], to: .CampignList)
To read data
DispatchQueue.global().async {
// GET OFFLINE DATA
if let object:[String:Any] = OfflineManager.sharedManager.loadCachedDataFromLocal(with: .CampignList) {
do {
let data = try JSONSerialization.data(withJSONObject: object, options: [])
let object = try CampaignListResponse.init(data: data)
self.arrCampignList = object.data ?? []
DispatchQueue.main.async {
self.tableVIew.reloadData()
}
} catch {
}
}
}
Note: You can define your own WSCacheKeys for type of your json like i am fetching some campaign list
You can use Realm or CoraData for saving data and showing it when you are offline.
Here is the official link for Realm.You can learn from here.
https://realm.io/docs/swift/latest

Resources