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

Swift file URL is nil when starting recording a video

当我开始录制视频时,它在隐式解包可选值时抛出“意外发现 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)。问题的原因可能是什么?图片库访问被拒绝是存储权限吗?

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

您声明:

private weak var videoOutput: AVCaptureMovieFileOutput!

不确定为什么在这里使用 weak,因为这个 class 拥有该对象并且应该保留一个强引用。

然后你在setUpCamera()中实例化一个本地版本的videoOutput:

let videoOutput = AVCaptureMovieFileOutput()

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

videoOutput.startRecording(to: …) 

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

self.videoOutput = videoOutput

或者更好的做法是在 class 声明中创建实例。

private var videoOutput: AVCaptureMovieFileOutput = AVCaptureMovieFileOutput()