// // ZLCustomCamera.swift // ZLPhotoBrowser // // Created by long on 2020/8/11. // // Copyright (c) 2020 Long Zhang <495181165@qq.com> // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import AVFoundation import CoreMotion open class ZLCustomCamera: UIViewController { enum Layout { static let bottomViewH: CGFloat = 120 static let largeCircleRadius: CGFloat = 80 static let smallCircleRadius: CGFloat = 65 static let largeCircleRecordScale: CGFloat = 1.2 static let smallCircleRecordScale: CGFloat = 0.5 static let borderLayerWidth: CGFloat = 1.8 static let animateLayerWidth: CGFloat = 5 static let cameraBtnNormalColor: UIColor = .white static let cameraBtnRecodingBorderColor: UIColor = .white.withAlphaComponent(0.8) } @objc public var takeDoneBlock: ((UIImage?, URL?) -> Void)? @objc public var cancelBlock: (() -> Void)? public lazy var tipsLabel: UILabel = { let label = UILabel() label.font = .zl.font(ofSize: 14) label.textColor = .white label.textAlignment = .center label.numberOfLines = 2 label.lineBreakMode = .byWordWrapping label.alpha = 0 return label }() public lazy var bottomView = UIView() public lazy var largeCircleView: UIView = { let view = UIView() view.layer.addSublayer(borderLayer) return view }() public lazy var smallCircleView: UIView = { let view = UIView() view.layer.masksToBounds = true view.layer.cornerRadius = ZLCustomCamera.Layout.smallCircleRadius / 2 view.isUserInteractionEnabled = false view.backgroundColor = ZLCustomCamera.Layout.cameraBtnNormalColor return view }() public lazy var borderLayer: CAShapeLayer = { let animateLayerRadius = ZLCustomCamera.Layout.largeCircleRadius let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: animateLayerRadius, height: animateLayerRadius), cornerRadius: animateLayerRadius / 2) let layer = CAShapeLayer() layer.path = path.cgPath layer.strokeColor = ZLCustomCamera.Layout.cameraBtnNormalColor.cgColor layer.fillColor = UIColor.clear.cgColor layer.lineWidth = ZLCustomCamera.Layout.borderLayerWidth return layer }() public lazy var animateLayer: CAShapeLayer = { let animateLayerRadius = ZLCustomCamera.Layout.largeCircleRadius let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: animateLayerRadius, height: animateLayerRadius), cornerRadius: animateLayerRadius / 2) let layer = CAShapeLayer() layer.path = path.cgPath layer.strokeColor = UIColor.zl.cameraRecodeProgressColor.cgColor layer.fillColor = UIColor.clear.cgColor layer.lineWidth = ZLCustomCamera.Layout.animateLayerWidth layer.lineCap = .round return layer }() public lazy var retakeBtn: ZLEnlargeButton = { let btn = ZLEnlargeButton(type: .custom) btn.setImage(.zl.getImage("zl_retake"), for: .normal) btn.addTarget(self, action: #selector(retakeBtnClick), for: .touchUpInside) btn.isHidden = true btn.adjustsImageWhenHighlighted = false btn.enlargeInset = 30 return btn }() public lazy var doneBtn: UIButton = { let btn = UIButton(type: .custom) btn.titleLabel?.font = ZLLayout.bottomToolTitleFont btn.setTitle(localLanguageTextValue(.done), for: .normal) btn.setTitleColor(.zl.bottomToolViewDoneBtnNormalTitleColor, for: .normal) btn.backgroundColor = .zl.bottomToolViewBtnNormalBgColor btn.addTarget(self, action: #selector(doneBtnClick), for: .touchUpInside) btn.isHidden = true btn.layer.masksToBounds = true btn.layer.cornerRadius = ZLLayout.bottomToolBtnCornerRadius return btn }() public lazy var dismissBtn: ZLEnlargeButton = { let btn = ZLEnlargeButton(type: .custom) btn.setImage(.zl.getImage("zl_camera_close"), for: .normal) btn.addTarget(self, action: #selector(dismissBtnClick), for: .touchUpInside) btn.adjustsImageWhenHighlighted = false btn.enlargeInset = 30 return btn }() public lazy var flashBtn: ZLEnlargeButton = { let btn = ZLEnlargeButton(type: .custom) btn.setImage(.zl.getImage("zl_flash_off"), for: .normal) btn.setImage(.zl.getImage("zl_flash_on"), for: .selected) btn.addTarget(self, action: #selector(flashBtnClick), for: .touchUpInside) btn.adjustsImageWhenHighlighted = false btn.enlargeInset = 30 return btn }() public lazy var switchCameraBtn: ZLEnlargeButton = { let cameraCount = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .unspecified).devices.count let btn = ZLEnlargeButton(type: .custom) btn.setImage(.zl.getImage("zl_toggle_camera"), for: .normal) btn.addTarget(self, action: #selector(switchCameraBtnClick), for: .touchUpInside) btn.adjustsImageWhenHighlighted = false btn.enlargeInset = 30 btn.isHidden = !cameraConfig.allowSwitchCamera || cameraCount <= 1 return btn }() public lazy var focusCursorView: UIImageView = { let view = UIImageView(image: .zl.getImage("zl_focus")) view.contentMode = .scaleAspectFit view.clipsToBounds = true view.frame = CGRect(x: 0, y: 0, width: 70, height: 70) view.alpha = 0 return view }() public lazy var takedImageView: UIImageView = { let view = UIImageView() view.backgroundColor = .black view.isHidden = true view.contentMode = .scaleAspectFit return view }() private var hideTipsTimer: Timer? private var takedImage: UIImage? private var videoUrl: URL? private var motionManager: CMMotionManager? private var orientation: AVCaptureVideoOrientation = .portrait private var torchDevice = AVCaptureDevice.default(for: .video) private let sessionQueue = DispatchQueue(label: "com.zl.camera.sessionQueue") private let session = AVCaptureSession() private var videoInput: AVCaptureDeviceInput? private var imageOutput: AVCapturePhotoOutput? private var movieFileOutput: AVCaptureMovieFileOutput? private var previewLayer: AVCaptureVideoPreviewLayer? private var recordVideoPlayerLayer: AVPlayerLayer? private var cameraConfigureFinish = false private var shouldLayout = true private var dragStart = false private var viewDidAppearCount = 0 private var restartRecordAfterSwitchCamera = false private var cacheVideoOrientation: AVCaptureVideoOrientation = .portrait private var recordUrls: [URL] = [] private var recordDurations: [Double] = [] private var microPhontIsAvailable = true private lazy var focusCursorTapGes: UITapGestureRecognizer = { let tap = UITapGestureRecognizer() tap.addTarget(self, action: #selector(adjustFocusPoint)) tap.delegate = self return tap }() private var cameraFocusPanGes: UIPanGestureRecognizer? private var recordLongGes: UILongPressGestureRecognizer? /// 是否正在调整焦距 private var isAdjustingFocusPoint = false /// 是否正在拍照 private var isTakingPicture = false private var showFlashBtn = true { didSet { flashBtn.isHidden = !showFlashBtn } } private lazy var cameraConfig = ZLPhotoConfiguration.default().cameraConfiguration // 仅支持竖屏 override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait } override public var prefersStatusBarHidden: Bool { return true } deinit { zl_debugPrint("ZLCustomCamera deinit") cleanTimer() try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) } @objc public init() { super.init(nibName: nil, bundle: nil) modalPresentationStyle = .fullScreen } @available(*, unavailable) public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override open func viewDidLoad() { super.viewDidLoad() setupUI() if !UIImagePickerController.isSourceTypeAvailable(.camera) { return } AVCaptureDevice.requestAccess(for: .video) { videoGranted in guard videoGranted else { ZLMainAsync(after: 1) { self.showAlertAndDismissAfterDoneAction(message: String(format: localLanguageTextValue(.noCameraAuthority), getAppName()), type: .camera) } return } guard self.cameraConfig.allowRecordVideo else { self.addNotification() return } AVCaptureDevice.requestAccess(for: .audio) { audioGranted in self.addNotification() if !audioGranted { ZLMainAsync(after: 1) { self.showNoMicrophoneAuthorityAlert() } } } } if cameraConfig.allowRecordVideo { do { try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .videoRecording, options: .mixWithOthers) try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation) } catch { let err = error as NSError if err.code == AVAudioSession.ErrorCode.insufficientPriority.rawValue || err.code == AVAudioSession.ErrorCode.isBusy.rawValue { microPhontIsAvailable = false } } } setupCamera() } override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) observerDeviceMotion() } override open func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if !UIImagePickerController.isSourceTypeAvailable(.camera) { showAlertAndDismissAfterDoneAction(message: localLanguageTextValue(.cameraUnavailable), type: .camera) } else if !cameraConfig.allowTakePhoto, !cameraConfig.allowRecordVideo { #if DEBUG fatalError("Error configuration of camera") #else showAlertAndDismissAfterDoneAction(message: "Error configuration of camera", type: nil) #endif } else if cameraConfigureFinish, viewDidAppearCount == 0 { showTipsLabel(message: cameraUsageTipsText()) let animation = ZLAnimationUtils.animation(type: .fade, fromValue: 0, toValue: 1, duration: 0.15) previewLayer?.add(animation, forKey: nil) setFocusCusor(point: view.center) } viewDidAppearCount += 1 } override open func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) motionManager?.stopDeviceMotionUpdates() motionManager = nil } override open func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) guard session.isRunning else { return } sessionQueue.async { self.session.stopRunning() } } override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) shouldLayout = true } override open func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() guard shouldLayout else { return } shouldLayout = false var insets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0) if #available(iOS 11.0, *) { insets = self.view.safeAreaInsets } let cameraRatio: CGFloat = 16 / 9 let layerH = min(view.zl.width * cameraRatio, view.zl.height) let previewLayerY: CGFloat if isSmallScreen() { previewLayerY = deviceIsFringeScreen() ? min(94, view.zl.height - layerH) : 0 } else { previewLayerY = 0 } let previewFrame = CGRect(x: 0, y: previewLayerY, width: view.bounds.width, height: layerH) previewLayer?.frame = previewFrame recordVideoPlayerLayer?.frame = previewFrame takedImageView.frame = previewFrame dismissBtn.frame = CGRect(x: 20, y: 60, width: 30, height: 30) retakeBtn.frame = CGRect(x: 20, y: 60, width: 28, height: 28) var bottomViewToBottomSpacing = view.zl.height - insets.bottom - ZLCustomCamera.Layout.bottomViewH if view.zl.height <= 812 { bottomViewToBottomSpacing -= deviceIsFringeScreen() ? 40 : 20 } bottomView.frame = CGRect(x: 0, y: bottomViewToBottomSpacing, width: view.bounds.width, height: ZLCustomCamera.Layout.bottomViewH) let largeCircleH = ZLCustomCamera.Layout.largeCircleRadius largeCircleView.frame = CGRect(x: (view.bounds.width - largeCircleH) / 2, y: (ZLCustomCamera.Layout.bottomViewH - largeCircleH) / 2, width: largeCircleH, height: largeCircleH) let smallCircleH = ZLCustomCamera.Layout.smallCircleRadius smallCircleView.frame = CGRect(x: (view.bounds.width - smallCircleH) / 2, y: (ZLCustomCamera.Layout.bottomViewH - smallCircleH) / 2, width: smallCircleH, height: smallCircleH) flashBtn.frame = CGRect(x: 60, y: (ZLCustomCamera.Layout.bottomViewH - 25) / 2, width: 25, height: 25) switchCameraBtn.frame = CGRect(x: bottomView.zl.width - 60 - 25, y: flashBtn.zl.top, width: 25, height: 25) let tipsTextHeight = (tipsLabel.text ?? " ").zl .boundingRect( font: .zl.font(ofSize: 14), limitSize: CGSize(width: view.bounds.width - 20, height: .greatestFiniteMagnitude) ) .height + 30 tipsLabel.frame = CGRect(x: 10, y: bottomView.frame.minY - tipsTextHeight, width: view.bounds.width - 20, height: tipsTextHeight) let doneBtnW = localLanguageTextValue(.done).zl .boundingRect( font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 40) ) .width + 20 let doneBtnY = view.bounds.height - 57 - insets.bottom doneBtn.frame = CGRect(x: view.bounds.width - doneBtnW - 20, y: doneBtnY, width: doneBtnW, height: ZLLayout.bottomToolBtnH) } private func setupUI() { view.backgroundColor = .black view.addSubview(dismissBtn) view.addSubview(takedImageView) view.addSubview(focusCursorView) view.addSubview(tipsLabel) view.addSubview(bottomView) bottomView.addSubview(flashBtn) bottomView.addSubview(largeCircleView) bottomView.addSubview(smallCircleView) bottomView.addSubview(switchCameraBtn) var takePictureTap: UITapGestureRecognizer? if cameraConfig.allowTakePhoto { takePictureTap = UITapGestureRecognizer(target: self, action: #selector(takePicture)) largeCircleView.addGestureRecognizer(takePictureTap!) } if cameraConfig.allowRecordVideo { let longGes = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(_:))) longGes.minimumPressDuration = 0.3 longGes.delegate = self largeCircleView.addGestureRecognizer(longGes) takePictureTap?.require(toFail: longGes) recordLongGes = longGes let panGes = UIPanGestureRecognizer(target: self, action: #selector(adjustCameraFocus(_:))) panGes.delegate = self panGes.maximumNumberOfTouches = 1 largeCircleView.addGestureRecognizer(panGes) cameraFocusPanGes = panGes recordVideoPlayerLayer = AVPlayerLayer() recordVideoPlayerLayer?.backgroundColor = UIColor.black.cgColor recordVideoPlayerLayer?.videoGravity = .resizeAspect recordVideoPlayerLayer?.isHidden = true view.layer.insertSublayer(recordVideoPlayerLayer!, at: 0) NotificationCenter.default.addObserver(self, selector: #selector(recordVideoPlayFinished), name: .AVPlayerItemDidPlayToEndTime, object: nil) } view.addSubview(retakeBtn) view.addSubview(doneBtn) // 预览layer previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer?.videoGravity = .resizeAspectFill previewLayer?.opacity = 0 view.layer.masksToBounds = true view.layer.insertSublayer(previewLayer!, at: 0) view.addGestureRecognizer(focusCursorTapGes) let pinchGes = UIPinchGestureRecognizer(target: self, action: #selector(pinchToAdjustCameraFocus(_:))) view.addGestureRecognizer(pinchGes) } private func observerDeviceMotion() { if !Thread.isMainThread { ZLMainAsync { self.observerDeviceMotion() } return } motionManager = CMMotionManager() motionManager?.deviceMotionUpdateInterval = 0.5 if motionManager?.isDeviceMotionAvailable == true { motionManager?.startDeviceMotionUpdates(to: .main, withHandler: { motion, _ in if let motion = motion { self.handleDeviceMotion(motion) } }) } else { motionManager = nil } } func handleDeviceMotion(_ motion: CMDeviceMotion) { let x = motion.gravity.x let y = motion.gravity.y if abs(y) >= abs(x) || abs(x) < 0.45 { if y >= 0.45 { orientation = .portraitUpsideDown } else { orientation = .portrait } } else { if x >= 0 { orientation = .landscapeLeft } else { orientation = .landscapeRight } } } private func setupCamera() { sessionQueue.async { let cameraConfig = ZLPhotoConfiguration.default().cameraConfiguration guard let camera = self.getCamera(position: cameraConfig.devicePosition.avDevicePosition) else { return } guard let input = try? AVCaptureDeviceInput(device: camera) else { return } self.session.beginConfiguration() // 相机画面输入流 self.videoInput = input let preset = cameraConfig.sessionPreset.avSessionPreset if self.session.canSetSessionPreset(preset) { self.session.sessionPreset = preset } else { self.session.sessionPreset = .photo } let movieFileOutput = AVCaptureMovieFileOutput() // 解决视频录制超过10s没有声音的bug movieFileOutput.movieFragmentInterval = .invalid self.movieFileOutput = movieFileOutput // 添加视频输入 if let videoInput = self.videoInput, self.session.canAddInput(videoInput) { self.session.addInput(videoInput) } // 添加音频输入 self.addAudioInput() // 照片输出流 let imageOutput = AVCapturePhotoOutput() self.imageOutput = imageOutput // 将输出流添加到session if self.session.canAddOutput(imageOutput) { self.session.addOutput(imageOutput) } if self.session.canAddOutput(movieFileOutput) { self.session.addOutput(movieFileOutput) } // imageOutPut添加到session之后才能判断supportedFlashModes if !cameraConfig.showFlashSwitch || !imageOutput.supportedFlashModes.contains(.on) { ZLMainAsync { self.showFlashBtn = false } } self.session.commitConfiguration() self.cameraConfigureFinish = true self.session.startRunning() } } private func getCamera(position: AVCaptureDevice.Position) -> AVCaptureDevice? { let devices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices for device in devices { if device.position == position { return device } } return nil } private func getMicrophone() -> AVCaptureDevice? { return AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInMicrophone], mediaType: .audio, position: .unspecified).devices.first } private func addAudioInput() { guard cameraConfig.allowRecordVideo else { return } // 音频输入流 var audioInput: AVCaptureDeviceInput? if let microphone = getMicrophone() { audioInput = try? AVCaptureDeviceInput(device: microphone) } guard microPhontIsAvailable, let ai = audioInput else { return } removeAudioInput() if session.isRunning { session.beginConfiguration() } if session.canAddInput(ai) { session.addInput(ai) } if session.isRunning { session.commitConfiguration() } } private func removeAudioInput() { var audioInput: AVCaptureInput? for input in session.inputs { if (input as? AVCaptureDeviceInput)?.device.deviceType == .builtInMicrophone { audioInput = input } } guard let audioInput = audioInput else { return } if session.isRunning { session.beginConfiguration() } session.removeInput(audioInput) if session.isRunning { session.commitConfiguration() } } private func addNotification() { NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil) if cameraConfig.allowRecordVideo { NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleAudioSessionInterruption), name: AVAudioSession.interruptionNotification, object: nil) } } private func showNoMicrophoneAuthorityAlert() { let continueAction = ZLCustomAlertAction(title: localLanguageTextValue(.keepRecording), style: .default, handler: nil) let gotoSettingsAction = ZLCustomAlertAction(title: localLanguageTextValue(.gotoSettings), style: .tint) { _ in guard let url = URL(string: UIApplication.openSettingsURLString) else { return } if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } } showAlertController(title: nil, message: String(format: localLanguageTextValue(.noMicrophoneAuthority), getAppName()), style: .alert, actions: [continueAction, gotoSettingsAction], sender: self) } private func showAlertAndDismissAfterDoneAction(message: String, type: ZLNoAuthorityType?) { let action = ZLCustomAlertAction(title: localLanguageTextValue(.done), style: .default) { [weak self] _ in self?.dismiss(animated: true) { if let type = type { ZLPhotoConfiguration.default().noAuthorityCallback?(type) } } } showAlertController(title: nil, message: message, style: .alert, actions: [action], sender: self) } private func cameraUsageTipsText() -> String { if cameraConfig.allowTakePhoto, cameraConfig.allowRecordVideo { return localLanguageTextValue(.customCameraTips) } else if cameraConfig.allowTakePhoto { return localLanguageTextValue(.customCameraTakePhotoTips) } else if cameraConfig.allowRecordVideo { return localLanguageTextValue(.customCameraRecordVideoTips) } else { return "" } } private func showTipsLabel(message: String, animated: Bool = true) { tipsLabel.layer.removeAllAnimations() tipsLabel.text = message if animated { UIView.animate(withDuration: 0.25) { self.tipsLabel.alpha = 1 } } else { tipsLabel.alpha = 1 } startHideTipsLabelTimer() } private func hideTipsLabel(animated: Bool = true) { tipsLabel.layer.removeAllAnimations() if animated { UIView.animate(withDuration: 0.25) { self.tipsLabel.alpha = 0 } } else { tipsLabel.alpha = 0 } } @objc private func hideTipsLabel_timerFunc() { cleanTimer() hideTipsLabel() } private func startHideTipsLabelTimer() { cleanTimer() hideTipsTimer = Timer.scheduledTimer(timeInterval: 3, target: ZLWeakProxy(target: self), selector: #selector(hideTipsLabel_timerFunc), userInfo: nil, repeats: false) RunLoop.current.add(hideTipsTimer!, forMode: .common) } private func cleanTimer() { hideTipsTimer?.invalidate() hideTipsTimer = nil } @objc private func appWillResignActive() { if session.isRunning { dismiss(animated: true, completion: nil) } if videoUrl != nil, let player = recordVideoPlayerLayer?.player { player.pause() } } @objc private func appDidBecomeActive() { if videoUrl != nil, let player = recordVideoPlayerLayer?.player { player.play() } } @objc private func handleAudioSessionInterruption(_ notify: Notification) { guard recordVideoPlayerLayer?.isHidden == false, let player = recordVideoPlayerLayer?.player else { return } guard player.rate == 0 else { return } let type = notify.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt let option = notify.userInfo?[AVAudioSessionInterruptionOptionKey] as? UInt if type == AVAudioSession.InterruptionType.ended.rawValue, option == AVAudioSession.InterruptionOptions.shouldResume.rawValue { player.play() } } @objc private func dismissBtnClick() { dismiss(animated: true) { self.cancelBlock?() } } @objc private func retakeBtnClick() { sessionQueue.async { self.session.startRunning() self.resetSubViewStatus() } takedImage = nil stopRecordAnimation() if let videoUrl = videoUrl { recordVideoPlayerLayer?.player?.pause() recordVideoPlayerLayer?.player = nil recordVideoPlayerLayer?.isHidden = true self.videoUrl = nil try? FileManager.default.removeItem(at: videoUrl) } } @objc private func flashBtnClick() { flashBtn.isSelected.toggle() } @objc private func switchCameraBtnClick() { guard !restartRecordAfterSwitchCamera else { return } guard let currInput = videoInput, let movieFileOutput = movieFileOutput else { return } if movieFileOutput.isRecording { let pauseTime = animateLayer.convertTime(CACurrentMediaTime(), from: nil) animateLayer.speed = 0 animateLayer.timeOffset = pauseTime restartRecordAfterSwitchCamera = true } sessionQueue.async { do { var newVideoInput: AVCaptureDeviceInput? if currInput.device.position == .back, let front = self.getCamera(position: .front) { newVideoInput = try AVCaptureDeviceInput(device: front) } else if currInput.device.position == .front, let back = self.getCamera(position: .back) { newVideoInput = try AVCaptureDeviceInput(device: back) } else { return } if let newVideoInput = newVideoInput { self.session.beginConfiguration() self.session.removeInput(currInput) if self.session.canAddInput(newVideoInput) { self.session.addInput(newVideoInput) self.videoInput = newVideoInput } else { self.session.addInput(currInput) } self.session.commitConfiguration() } } catch { zl_debugPrint("切换摄像头失败 \(error.localizedDescription)") } } } private func canEditImage() -> Bool { let config = ZLPhotoConfiguration.default() guard config.allowEditImage else { return false } // 如果满足如下条件,则会在拍照完成后,返回相册界面直接进入编辑界面,这里就不在编辑 let editAfterSelect = config.editAfterSelectThumbnailImage && config.maxSelectCount == 1 return !editAfterSelect } @objc private func editImage() { guard let takedImage = takedImage, canEditImage() else { return } ZLEditImageViewController.showEditImageVC(parentVC: self, image: takedImage) { [weak self] in self?.retakeBtnClick() } completion: { [weak self] editImage, _ in self?.takedImage = editImage self?.takedImageView.image = editImage self?.doneBtnClick() } } @objc private func doneBtnClick() { recordVideoPlayerLayer?.player?.pause() // 置为nil会导致卡顿,先注释,不影响内存释放 // self.recordVideoPlayerLayer?.player = nil dismiss(animated: true) { self.takeDoneBlock?(self.takedImage, self.videoUrl) } } // 点击拍照 @objc private func takePicture() { guard ZLPhotoManager.hasCameraAuthority(), !isTakingPicture else { return } guard let imageOutput = imageOutput else { return } guard session.outputs.contains(imageOutput) else { showAlertAndDismissAfterDoneAction(message: localLanguageTextValue(.cameraUnavailable), type: .camera) return } isTakingPicture = true let connection = imageOutput.connection(with: .video) connection?.videoOrientation = orientation if videoInput?.device.position == .front, connection?.isVideoMirroringSupported == true { connection?.isVideoMirrored = true } let setting = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecJPEG]) if videoInput?.device.hasFlash == true, flashBtn.isSelected { setting.flashMode = .on } else { setting.flashMode = .off } imageOutput.capturePhoto(with: setting, delegate: self) } // 长按录像 @objc private func longPressAction(_ longGes: UILongPressGestureRecognizer) { if longGes.state == .began { guard ZLPhotoManager.hasCameraAuthority() else { return } startRecord() } else if longGes.state == .cancelled || longGes.state == .ended { finishRecord() } } // 调整焦点 @objc private func adjustFocusPoint(_ tap: UITapGestureRecognizer) { guard session.isRunning, !isAdjustingFocusPoint else { return } let point = tap.location(in: view) if point.y > bottomView.frame.minY - 30 { return } setFocusCusor(point: point) } private func setFocusCusor(point: CGPoint) { animateFocusCursor(point: point) // UI坐标转换为摄像头坐标 let cameraPoint = previewLayer?.captureDevicePointConverted(fromLayerPoint: point) ?? view.center focusCamera( mode: ZLPhotoConfiguration.default().cameraConfiguration.focusMode.avFocusMode, exposureMode: ZLPhotoConfiguration.default().cameraConfiguration.exposureMode.avFocusMode, point: cameraPoint ) } private func animateFocusCursor(point: CGPoint) { isAdjustingFocusPoint = true focusCursorView.center = point focusCursorView.alpha = 1 let scaleAnimation = ZLAnimationUtils.animation(type: .scale, fromValue: 2, toValue: 1, duration: 0.25) let fadeShowAnimation = ZLAnimationUtils.animation(type: .fade, fromValue: 0, toValue: 1, duration: 0.25) let fadeDismissAnimation = ZLAnimationUtils.animation(type: .fade, fromValue: 1, toValue: 0, duration: 0.25) fadeDismissAnimation.beginTime = 0.75 let group = CAAnimationGroup() group.animations = [scaleAnimation, fadeShowAnimation, fadeDismissAnimation] group.duration = 1 group.delegate = self group.fillMode = .forwards group.isRemovedOnCompletion = false focusCursorView.layer.add(group, forKey: nil) } // 调整焦距 @objc private func adjustCameraFocus(_ pan: UIPanGestureRecognizer) { let convertRect = bottomView.convert(largeCircleView.frame, to: view) let point = pan.location(in: view) if pan.state == .began { dragStart = true startRecord() } else if pan.state == .changed { guard dragStart else { return } let maxZoomFactor = getMaxZoomFactor() var zoomFactor = (convertRect.midY - point.y) / convertRect.midY * maxZoomFactor zoomFactor = max(1, min(zoomFactor, maxZoomFactor)) setVideoZoomFactor(zoomFactor) } else if pan.state == .cancelled || pan.state == .ended { guard dragStart else { return } dragStart = false finishRecord() } } @objc private func pinchToAdjustCameraFocus(_ pinch: UIPinchGestureRecognizer) { guard let device = videoInput?.device else { return } var zoomFactor = device.videoZoomFactor * pinch.scale zoomFactor = max(1, min(zoomFactor, getMaxZoomFactor())) setVideoZoomFactor(zoomFactor) pinch.scale = 1 } private func getMaxZoomFactor() -> CGFloat { guard let device = videoInput?.device else { return 1 } if #available(iOS 11.0, *) { return min(15, device.maxAvailableVideoZoomFactor) } else { return min(15, device.activeFormat.videoMaxZoomFactor) } } private func setVideoZoomFactor(_ zoomFactor: CGFloat) { guard let device = videoInput?.device else { return } do { try device.lockForConfiguration() device.videoZoomFactor = zoomFactor device.unlockForConfiguration() } catch { zl_debugPrint("调整焦距失败 \(error.localizedDescription)") } } private func focusCamera(mode: AVCaptureDevice.FocusMode, exposureMode: AVCaptureDevice.ExposureMode, point: CGPoint) { do { guard let device = videoInput?.device else { return } try device.lockForConfiguration() if device.isFocusModeSupported(mode) { device.focusMode = mode } if device.isFocusPointOfInterestSupported { device.focusPointOfInterest = point } if device.isExposureModeSupported(exposureMode) { device.exposureMode = exposureMode } if device.isExposurePointOfInterestSupported { device.exposurePointOfInterest = point } device.unlockForConfiguration() } catch { zl_debugPrint("相机聚焦设置失败 \(error.localizedDescription)") } } // 打开手电筒 private func openTorch() { guard flashBtn.isSelected, torchDevice?.isTorchAvailable == true, torchDevice?.torchMode == .off else { return } sessionQueue.async { do { try self.torchDevice?.lockForConfiguration() self.torchDevice?.torchMode = .on self.torchDevice?.unlockForConfiguration() } catch { zl_debugPrint("打开手电筒失败 \(error.localizedDescription)") } } } // 关闭手电筒 private func closeTorch() { guard flashBtn.isSelected, torchDevice?.isTorchAvailable == true, torchDevice?.torchMode == .on else { return } sessionQueue.async { do { try self.torchDevice?.lockForConfiguration() self.torchDevice?.torchMode = .off self.torchDevice?.unlockForConfiguration() } catch { zl_debugPrint("关闭手电筒失败 \(error.localizedDescription)") } } } private func startRecord() { guard let movieFileOutput = movieFileOutput else { return } guard !movieFileOutput.isRecording else { return } guard session.outputs.contains(movieFileOutput) else { showAlertAndDismissAfterDoneAction(message: localLanguageTextValue(.cameraUnavailable), type: .camera) return } dismissBtn.isHidden = true flashBtn.isHidden = true let connection = movieFileOutput.connection(with: .video) connection?.videoScaleAndCropFactor = 1 if !restartRecordAfterSwitchCamera { connection?.videoOrientation = orientation cacheVideoOrientation = orientation } else { connection?.videoOrientation = cacheVideoOrientation } // 解决不同系统版本,因为录制视频编码导致安卓端无法播放的问题 if #available(iOS 11.0, *), movieFileOutput.availableVideoCodecTypes.contains(cameraConfig.videoCodecType), let connection = connection { let outputSettings = [AVVideoCodecKey: cameraConfig.videoCodecType] movieFileOutput.setOutputSettings(outputSettings, for: connection) } // 解决前置摄像头录制视频时候左右颠倒的问题 if videoInput?.device.position == .front { // 镜像设置 if connection?.isVideoMirroringSupported == true { connection?.isVideoMirrored = true } closeTorch() } else { openTorch() } let url = URL(fileURLWithPath: ZLVideoManager.getVideoExportFilePath()) movieFileOutput.startRecording(to: url, recordingDelegate: self) } private func finishRecord() { closeTorch() restartRecordAfterSwitchCamera = false guard let movieFileOutput = movieFileOutput else { return } guard movieFileOutput.isRecording else { return } try? AVAudioSession.sharedInstance().setCategory(.playback) movieFileOutput.stopRecording() } private func startRecordAnimation() { UIView.animate(withDuration: 0.1, animations: { self.largeCircleView.layer.transform = CATransform3DScale(CATransform3DIdentity, ZLCustomCamera.Layout.largeCircleRecordScale, ZLCustomCamera.Layout.largeCircleRecordScale, 1) self.smallCircleView.layer.transform = CATransform3DScale(CATransform3DIdentity, ZLCustomCamera.Layout.smallCircleRecordScale, ZLCustomCamera.Layout.smallCircleRecordScale, 1) self.borderLayer.strokeColor = ZLCustomCamera.Layout.cameraBtnRecodingBorderColor.cgColor self.borderLayer.lineWidth = ZLCustomCamera.Layout.animateLayerWidth }) { _ in self.largeCircleView.layer.addSublayer(self.animateLayer) let animation = CABasicAnimation(keyPath: "strokeEnd") animation.fromValue = 0 animation.toValue = 1 animation.duration = Double(self.cameraConfig.maxRecordDuration) animation.delegate = self self.animateLayer.add(animation, forKey: nil) } } private func stopRecordAnimation() { ZLMainAsync { self.borderLayer.strokeColor = ZLCustomCamera.Layout.cameraBtnNormalColor.cgColor self.borderLayer.lineWidth = ZLCustomCamera.Layout.borderLayerWidth self.animateLayer.speed = 1 self.animateLayer.timeOffset = 0 self.animateLayer.beginTime = 0 self.animateLayer.removeFromSuperlayer() self.animateLayer.removeAllAnimations() self.largeCircleView.transform = .identity self.smallCircleView.transform = .identity } } private func resetSubViewStatus() { ZLMainAsync { if self.session.isRunning { self.showTipsLabel(message: self.cameraUsageTipsText()) self.bottomView.isHidden = false self.dismissBtn.isHidden = false self.flashBtn.isHidden = !self.showFlashBtn self.retakeBtn.isHidden = true self.doneBtn.isHidden = true self.takedImageView.isHidden = true self.takedImage = nil } else { self.hideTipsLabel() self.bottomView.isHidden = true self.dismissBtn.isHidden = true if self.takedImage != nil { let canEdit = self.canEditImage() self.retakeBtn.isHidden = canEdit self.doneBtn.isHidden = canEdit } else { self.retakeBtn.isHidden = false self.doneBtn.isHidden = false } } } } private func playRecordVideo(fileUrl: URL) { recordVideoPlayerLayer?.isHidden = false let player = AVPlayer(url: fileUrl) player.automaticallyWaitsToMinimizeStalling = false recordVideoPlayerLayer?.player = player player.play() } @objc private func recordVideoPlayFinished() { recordVideoPlayerLayer?.player?.seek(to: .zero) recordVideoPlayerLayer?.player?.play() } } extension ZLCustomCamera: AVCapturePhotoCaptureDelegate { public func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) { ZLMainAsync { let animation = ZLAnimationUtils.animation(type: .fade, fromValue: 0, toValue: 1, duration: 0.25) self.previewLayer?.add(animation, forKey: nil) } } public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?, previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) { ZLMainAsync { defer { self.isTakingPicture = false } if photoSampleBuffer == nil || error != nil { zl_debugPrint("拍照失败 \(error?.localizedDescription ?? "")") return } if let data = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: photoSampleBuffer!, previewPhotoSampleBuffer: previewPhotoSampleBuffer) { self.sessionQueue.async { self.session.stopRunning() self.resetSubViewStatus() } self.takedImage = UIImage(data: data)?.zl.fixOrientation() self.takedImageView.image = self.takedImage self.takedImageView.isHidden = false self.editImage() } else { zl_debugPrint("拍照失败,data为空") } } } } extension ZLCustomCamera: AVCaptureFileOutputRecordingDelegate { public func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) { guard recordLongGes?.state != .possible else { finishRecordAndMergeVideo() return } if restartRecordAfterSwitchCamera { restartRecordAfterSwitchCamera = false ZLMainAsync() { let pauseTime = self.animateLayer.timeOffset self.animateLayer.speed = 1 self.animateLayer.timeOffset = 0 self.animateLayer.beginTime = 0 let timeSincePause = self.animateLayer.convertTime(CACurrentMediaTime(), from: nil) - pauseTime self.animateLayer.beginTime = timeSincePause } } else { ZLMainAsync { self.startRecordAnimation() } } } public func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { ZLMainAsync { self.recordUrls.append(outputFileURL) self.recordDurations.append(output.recordedDuration.seconds) if self.restartRecordAfterSwitchCamera { self.startRecord() return } self.finishRecordAndMergeVideo() } } private func finishRecordAndMergeVideo() { ZLMainAsync { self.stopRecordAnimation() defer { self.resetSubViewStatus() } guard !self.recordUrls.isEmpty else { return } let duration = self.recordDurations.reduce(0, +) // 重置焦距 self.setVideoZoomFactor(1) if duration < Double(self.cameraConfig.minRecordDuration) { showAlertView(String(format: localLanguageTextValue(.minRecordTimeTips), self.cameraConfig.minRecordDuration), self) self.recordUrls.forEach { try? FileManager.default.removeItem(at: $0) } self.recordUrls.removeAll() self.recordDurations.removeAll() return } self.session.stopRunning() // 拼接视频 if self.recordUrls.count > 1 { let hud = ZLProgressHUD.show() ZLVideoManager.mergeVideos(fileUrls: self.recordUrls) { [weak self] url, error in hud.hide() if let url = url, error == nil { self?.videoUrl = url self?.playRecordVideo(fileUrl: url) } else if let error = error { self?.videoUrl = nil showAlertView(error.localizedDescription, self) } self?.recordUrls.forEach { try? FileManager.default.removeItem(at: $0) } self?.recordUrls.removeAll() self?.recordDurations.removeAll() } } else { let url = self.recordUrls[0] self.videoUrl = url self.playRecordVideo(fileUrl: url) self.recordUrls.removeAll() self.recordDurations.removeAll() } } } } extension ZLCustomCamera: CAAnimationDelegate { public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { if anim is CAAnimationGroup { focusCursorView.alpha = 0 focusCursorView.layer.removeAllAnimations() isAdjustingFocusPoint = false } else { finishRecord() } } } extension ZLCustomCamera: UIGestureRecognizerDelegate { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { let gesTuples: [(UIGestureRecognizer?, UIGestureRecognizer?)] = [(recordLongGes, cameraFocusPanGes), (recordLongGes, focusCursorTapGes), (cameraFocusPanGes, focusCursorTapGes)] let result = gesTuples.map { ges1, ges2 in (ges1 == gestureRecognizer && ges2 == otherGestureRecognizer) || (ges2 == otherGestureRecognizer && ges1 == gestureRecognizer) }.filter { $0 == true } return !result.isEmpty } }