开始录制视频时 Swift 文件 URL 为零

Posted

技术标签:

【中文标题】开始录制视频时 Swift 文件 URL 为零【英文标题】:Swift file URL is nil when starting recording a video 【发布时间】:2019-10-01 10:51:42 【问题描述】:

当我开始录制视频时,它会在隐式展开 Optional value: file 时抛出 Unexpectedly found nil。摄像头和麦克风权限已启用并包含在 Info.plist 中。

/// UIViewController which displays create screen, camera preview.
class CreateViewController: UIViewController 

/// Delegate which will receive calls on create view controller camera changes.
var delegate: CreateViewControllerDelegate?

private weak var flashlightCameraButton: UIImageView!
private weak var recordingCameraButton: UIImageView!
private weak var switchCamerasCameraButton: UIImageView!

var viewModel: CreateViewModel?

// MARK: - Camera

private var captureSession: AVCaptureSession!
private weak var previewLayer: CALayer!
private weak var captureCamera: AVCaptureDevice!
private weak var audioDevice: AVCaptureDevice!
private weak var videoOutput: AVCaptureMovieFileOutput!
private var isFlashlightOn = false
private var isRecording = false
private var isFrontCamera = false

/// The URL of the challenge video
var challengeVideoURL: URL?

override func viewDidLoad() 
    super.viewDidLoad()
    setUp()


override func viewWillDisappear(_ animated: Bool) 
    super.viewWillDisappear(animated)
    if isRecording 
        videoOutput.stopRecording()
    


private func setUp() 
    setUpMainView()
    setUpCamera()
    setUpCameraButtons()


private func setUpMainView() 
    let appWindow = UIApplication.shared.keyWindow
    guard let window = appWindow else  return 
    view = UIView(frame: CGRect(x: 0, y: 0, width: window.frame.width, height: window.frame.height))


private func setUpCamera() 
    let captureSession = AVCaptureSession()
    self.captureSession = captureSession
    captureSession.sessionPreset = .iFrame1280x720
    setUpCameraSide(front: isFrontCamera)
    let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
    self.previewLayer = previewLayer
    view.layer.addSublayer(previewLayer)
    previewLayer.frame = view.layer.frame
    let videoOutput = AVCaptureMovieFileOutput()
    if captureSession.canAddOutput(videoOutput) 
        captureSession.addOutput(videoOutput)
    
    captureSession.startRunning()


private func setUpCameraSide(front: Bool) 
    captureSession.beginConfiguration()
    if let inputs = captureSession.inputs as? [AVCaptureDeviceInput] 
        for input in inputs 
            self.captureSession.removeInput(input)
        
    
    guard let availableDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: front ? .front : .back) else  return 
    // Prepare visual
    self.captureCamera = availableDevice
    do 
        let captureDeviceInput = try AVCaptureDeviceInput(device: captureCamera)
        captureSession.addInput(captureDeviceInput)
     catch let error 
        // - TODO: display the error
    
    // Prepare audio
    guard let audioDevice = AVCaptureDevice.default(for: .audio) else  return 
    self.audioDevice = audioDevice
    do 
        let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice)
        captureSession.addInput(audioDeviceInput)
     catch let error 
        // - TODO: display the error
    

    captureSession.commitConfiguration()


private func setUpCameraButtons() 
    let flashlightCameraButton = UIImageView()
    self.flashlightCameraButton = flashlightCameraButton
    view.addSubview(flashlightCameraButton)
    flashlightCameraButton.image = UIImage(named: "camera_flashlight")
    let flashlightTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(turnOnFlashlight))
    flashlightCameraButton.isUserInteractionEnabled = true
    flashlightCameraButton.addGestureRecognizer(flashlightTapGestureRecognizer)
    flashlightCameraButton.snp.makeConstraints  make in
        make.left.equalToSuperview().inset(25)
        make.bottom.equalToSuperview().inset(25)
        make.height.width.equalTo(50)
    

    let recordingCameraButton = UIImageView()
    self.recordingCameraButton = recordingCameraButton
    view.addSubview(recordingCameraButton)
    recordingCameraButton.image = UIImage(named: "camera_record")
    let recordingTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(recording))
    recordingCameraButton.isUserInteractionEnabled = true
    recordingCameraButton.addGestureRecognizer(recordingTapGestureRecognizer)
    recordingCameraButton.snp.makeConstraints  make in
        make.centerX.equalToSuperview()
        make.bottom.equalToSuperview().inset(25)
        make.height.width.equalTo(70)
    

    let switchCamerasCameraButton = UIImageView()
    self.switchCamerasCameraButton = switchCamerasCameraButton
    view.addSubview(switchCamerasCameraButton)
    switchCamerasCameraButton.image = UIImage(named: "camera_switch_camera")
    let switchCameraTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(switchCamera))
    switchCamerasCameraButton.isUserInteractionEnabled = true
    switchCamerasCameraButton.addGestureRecognizer(switchCameraTapGestureRecognizer)
    switchCamerasCameraButton.snp.makeConstraints  make in
        make.right.equalToSuperview().inset(25)
        make.bottom.equalToSuperview().inset(25)
        make.height.width.equalTo(50)
    


@objc private func turnOnFlashlight() 
    isFlashlightOn = !isFlashlightOn
    if !isRecording 
        if !isFrontCamera, captureCamera.hasTorch 
            do 
                try captureCamera.lockForConfiguration()
                captureCamera.torchMode = captureCamera.isTorchActive ? .on : .off
                captureCamera.unlockForConfiguration()
             catch let error 
                // - TODO: handle error here
            
        
    


@objc private func recording() 
    if !isRecording 
        delegate?.didStartRecording(self)
        NotificationCenter.default.post(name: .recordingStartedNotification, object: nil)
        if !captureSession.isRunning 
            return
        
        let paths = FileManager.default.urls(for: .moviesDirectory, in: .userDomainMask)
        let fileURL = paths[0].appendingPathComponent("challenge.mov")
        try? FileManager.default.removeItem(at: fileURL)
        challengeVideoURL = fileURL
        videoOutput.startRecording(to: fileURL, recordingDelegate: self)
     else 
        videoOutput.stopRecording()
    
    isRecording = !isRecording


@objc private func switchCamera() 
    isFrontCamera = !isFrontCamera
    setUpCameraSide(front: isFrontCamera)


private func setUpTimer() 




// MARK: - Challenge is recorded

extension CreateViewController: AVCaptureFileOutputRecordingDelegate 
    func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) 
        guard error == nil else  return 
        viewModel?.challengeVideoUrl = outputFileURL
        UISaveVideoAtPathToSavedPhotosAlbum(outputFileURL.path, nil, nil, nil)

    delegate?.didStopRecording(self)
    NotificationCenter.default.post(name: .recordingStoppedNotification, object: nil)
    // - TODO: after receiving this call moves user to challenge preview


致命错误出现在这一行:videoOutput.startRecording(to: fileURL, recordingDelegate: self)。问题的原因可能是什么?拒绝访问照片库是存储权限吗?

【问题讨论】:

我想知道你是否真的可以写.moviesDirectory。也许尝试使用临时路径? 我尝试了所有目录,但仍然出现同样的错误。 @Larme 临时路径给出了同样的错误。 【参考方案1】:

每次您在 Swift 中使用 ! 时,都会有一只小猫死亡 :-)。您的代码具有预期的行为。

您声明:

private weak var videoOutput: AVCaptureMovieFileOutput!

不知道你为什么在这里使用weak,因为这个类拥有对象并且应该保持一个强引用。

然后在setUpCamera() 中实例化videoOutput本地 版本:

let videoOutput = AVCaptureMovieFileOutput()

但永远不要将其分配给实例属性videoOutput。因此videoOutputnil 并且你在“崩溃”

videoOutput.startRecording(to: …) 

setUpCamera() 添加到捕获会话之后分配属性。

self.videoOutput = videoOutput

或者最好在类声明中创建实例。

private var videoOutput: AVCaptureMovieFileOutput = AVCaptureMovieFileOutput()

【讨论】:

酷,它有效,保持弱引用可能是我的错误,但无论如何它有所帮助,现在它完美无缺。真的非常感谢沃伦。 太棒了。 weak 是我故意使用的东西,通常在闭包和委托引用中保留循环。主要问题是所有权变更和 ARC 系统可以随时使弱引用为零。将其与强制解包结合起来,您可能会遇到未来的错误。

以上是关于开始录制视频时 Swift 文件 URL 为零的主要内容,如果未能解决你的问题,请参考以下文章

Swift IOS 使用 AVFoundation 录制视频和音频

从 URL 播放视频并同时通过摄像头录制视频 - Objective C

Swift - 检查 url 资产是不是有声音

无法使用 AVFoundation 将录制的视频上传到服务器 - Swift 3

Swift:从 url 保存视频数据时出错

在 iOS 中录制视频时播放音频文件