RPScreenRecorder.shared().isAvailable is always false

426 views Asked by At

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.

1

There are 1 answers

0
Walue On

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.