From 57eceb0aefc44165d5f89bdf845860b1afc67f90 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 27 May 2023 19:39:09 +0400 Subject: [PATCH] Camera and editor improvements --- submodules/Camera/Sources/Camera.swift | 20 +- submodules/Camera/Sources/CameraDevice.swift | 26 +- submodules/Camera/Sources/CameraOutput.swift | 25 +- .../Camera/Sources/CameraPreviewView.swift | 4 + submodules/Camera/Sources/VideoRecorder.swift | 595 +++++++++++++----- .../CameraScreen/Sources/CameraScreen.swift | 19 +- .../Sources/MediaEditorScreen.swift | 2 +- 7 files changed, 495 insertions(+), 196 deletions(-) diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index e57823a79a..3ac78d7fe3 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -142,8 +142,17 @@ private final class CameraContext { self.session.stopRunning() } - func focus(at point: CGPoint) { - self.device.setFocusPoint(point, focusMode: .continuousAutoFocus, exposureMode: .continuousAutoExposure, monitorSubjectAreaChange: true) + func focus(at point: CGPoint, autoFocus: Bool) { + let focusMode: AVCaptureDevice.FocusMode + let exposureMode: AVCaptureDevice.ExposureMode + if autoFocus { + focusMode = .continuousAutoFocus + exposureMode = .continuousAutoExposure + } else { + focusMode = .autoFocus + exposureMode = .autoExpose + } + self.device.setFocusPoint(point, focusMode: focusMode, exposureMode: exposureMode, monitorSubjectAreaChange: true) } func setFps(_ fps: Float64) { @@ -276,6 +285,9 @@ public final class Camera { self.metrics = Camera.Metrics(model: DeviceModel.current) let session = AVCaptureSession() + session.usesApplicationAudioSession = true + session.automaticallyConfiguresApplicationAudioSession = false + session.automaticallyConfiguresCaptureDeviceForWideColor = false if let previewView { previewView.session = session } @@ -373,10 +385,10 @@ public final class Camera { } } - public func focus(at point: CGPoint) { + public func focus(at point: CGPoint, autoFocus: Bool = true) { self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { - context.focus(at: point) + context.focus(at: point, autoFocus: autoFocus) } } } diff --git a/submodules/Camera/Sources/CameraDevice.swift b/submodules/Camera/Sources/CameraDevice.swift index 816ce061d9..880e4f35b4 100644 --- a/submodules/Camera/Sources/CameraDevice.swift +++ b/submodules/Camera/Sources/CameraDevice.swift @@ -8,9 +8,21 @@ private let defaultFPS: Double = 30.0 final class CameraDevice { var position: Camera.Position = .back + deinit { + if let videoDevice = self.videoDevice { + self.unsubscribeFromChanges(videoDevice) + } + } + public private(set) var videoDevice: AVCaptureDevice? = nil { didSet { + if let previousVideoDevice = oldValue { + self.unsubscribeFromChanges(previousVideoDevice) + } self.videoDevicePromise.set(.single(self.videoDevice)) + if let videoDevice = self.videoDevice { + self.subscribeForChanges(videoDevice) + } } } private var videoDevicePromise = Promise() @@ -93,12 +105,12 @@ final class CameraDevice { } } - private func subscribeForChanges() { - NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaChanged), name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: self.videoDevice) + private func subscribeForChanges(_ device: AVCaptureDevice) { + NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaChanged), name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: device) } - private func unsubscribeFromChanges() { - NotificationCenter.default.removeObserver(self, name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: self.videoDevice) + private func unsubscribeFromChanges(_ device: AVCaptureDevice) { + NotificationCenter.default.removeObserver(self, name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: device) } @objc private func subjectAreaChanged() { @@ -171,6 +183,12 @@ final class CameraDevice { device.focusPointOfInterest = point device.focusMode = focusMode } + + device.isSubjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange + + if abs(device.exposureTargetBias) > 0.0 { + device.setExposureTargetBias(0.0) + } } } diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index de12b7f7d6..6f3b2d0e58 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -109,11 +109,12 @@ final class CameraOutput: NSObject { func configureVideoStabilization() { if let videoDataOutputConnection = self.videoOutput.connection(with: .video), videoDataOutputConnection.isVideoStabilizationSupported { - if #available(iOS 13.0, *) { - videoDataOutputConnection.preferredVideoStabilizationMode = .cinematicExtended - } else { - videoDataOutputConnection.preferredVideoStabilizationMode = .cinematic - } + videoDataOutputConnection.preferredVideoStabilizationMode = .standard +// if #available(iOS 13.0, *) { +// videoDataOutputConnection.preferredVideoStabilizationMode = .cinematicExtended +// } else { +// videoDataOutputConnection.preferredVideoStabilizationMode = .cinematic +// } } } @@ -178,7 +179,7 @@ final class CameraOutput: NSObject { let outputFileName = NSUUID().uuidString let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4" let outputFileURL = URL(fileURLWithPath: outputFilePath) - let videoRecorder = VideoRecorder(preset: MediaPreset(videoSettings: videoSettings, audioSettings: audioSettings), videoTransform: CGAffineTransform(rotationAngle: .pi / 2.0), fileUrl: outputFileURL, completion: { [weak self] result in + let videoRecorder = VideoRecorder(configuration: VideoRecorder.Configuration(videoSettings: videoSettings, audioSettings: audioSettings), videoTransform: CGAffineTransform(rotationAngle: .pi / 2.0), fileUrl: outputFileURL, completion: { [weak self] result in if case .success = result { self?.recordingCompletionPipe.putNext(outputFilePath) } else { @@ -186,7 +187,8 @@ final class CameraOutput: NSObject { } }) - videoRecorder.start() + + videoRecorder?.start() self.videoRecorder = videoRecorder return Signal { subscriber in @@ -244,13 +246,8 @@ extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureA // self.processSampleBuffer?(finalVideoPixelBuffer, connection) // } - if let videoRecorder = self.videoRecorder, videoRecorder.isRecording || videoRecorder.isStopping { - let mediaType = sampleBuffer.type - if mediaType == kCMMediaType_Video { - videoRecorder.appendVideo(sampleBuffer: sampleBuffer) - } else if mediaType == kCMMediaType_Audio { - videoRecorder.appendAudio(sampleBuffer: sampleBuffer) - } + if let videoRecorder = self.videoRecorder, videoRecorder.isRecording { + videoRecorder.appendSampleBuffer(sampleBuffer) } } diff --git a/submodules/Camera/Sources/CameraPreviewView.swift b/submodules/Camera/Sources/CameraPreviewView.swift index 3361f257be..74ae7b8b3b 100644 --- a/submodules/Camera/Sources/CameraPreviewView.swift +++ b/submodules/Camera/Sources/CameraPreviewView.swift @@ -108,6 +108,10 @@ public class CameraSimplePreviewView: UIView { } |> distinctUntilChanged } + + public func cameraPoint(for location: CGPoint) -> CGPoint { + return self.videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: location) + } } public class CameraPreviewView: MTKView { diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index 234957074c..cd67763227 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -1,129 +1,439 @@ import Foundation import AVFoundation import SwiftSignalKit +import TelegramCore -struct MediaPreset { - var videoSettings: [String: Any] - var audioSettings: [String: Any] - - init(videoSettings: [String: Any], audioSettings: [String: Any]) { - self.videoSettings = videoSettings - self.audioSettings = audioSettings - } - - var hasAudio: Bool { - return !self.audioSettings.isEmpty +private extension CMSampleBuffer { + var endTime: CMTime { + let presentationTime = CMSampleBufferGetPresentationTimeStamp(self) + let duration = CMSampleBufferGetDuration(self) + return presentationTime + duration } } -final class VideoRecorder { +private final class VideoRecorderImpl { + public enum RecorderError: LocalizedError { + case generic + case avError(Error) + + public var errorDescription: String? { + switch self { + case .generic: + return "Error" + case let .avError(error): + return error.localizedDescription + } + } + } + + private let queue = DispatchQueue(label: "VideoRecorder") + + private var assetWriter: AVAssetWriter + private var videoInput: AVAssetWriterInput? + private var audioInput: AVAssetWriterInput? + + private var pendingAudioSampleBuffers: [CMSampleBuffer] = [] + + private var _duration: CMTime = .zero + public var duration: CMTime { + self.queue.sync { _duration } + } + + private var lastVideoSampleTime: CMTime = .invalid + private var recordingStartSampleTime: CMTime = .invalid + private var recordingStopSampleTime: CMTime = .invalid + + private let configuration: VideoRecorder.Configuration + private let videoTransform: CGAffineTransform + private let url: URL + fileprivate var completion: (Bool) -> Void = { _ in } + + private let error = Atomic(value: nil) + + private var stopped = false + private var hasAllVideoBuffers = false + private var hasAllAudioBuffers = false + + public init?(configuration: VideoRecorder.Configuration, videoTransform: CGAffineTransform, fileUrl: URL) { + self.configuration = configuration + self.videoTransform = videoTransform + self.url = fileUrl + + try? FileManager.default.removeItem(at: url) + guard let assetWriter = try? AVAssetWriter(url: url, fileType: .mp4) else { + return nil + } + self.assetWriter = assetWriter + self.assetWriter.shouldOptimizeForNetworkUse = false + } + + private func hasError() -> Error? { + return self.error.with { $0 } + } + + public func start() { + self.queue.async { + self.recordingStartSampleTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + } + } + + public func appendVideoSampleBuffer(_ sampleBuffer: CMSampleBuffer) { + if let _ = self.hasError() { + return + } + + guard let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer), CMFormatDescriptionGetMediaType(formatDescription) == kCMMediaType_Video else { + return + } + + let presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + self.queue.async { + guard !self.stopped && self.error.with({ $0 }) == nil else { + return + } + + var failed = false + if self.videoInput == nil { + let videoSettings = self.configuration.videoSettings + if self.assetWriter.canApply(outputSettings: videoSettings, forMediaType: .video) { + let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings, sourceFormatHint: formatDescription) + videoInput.expectsMediaDataInRealTime = true + videoInput.transform = self.videoTransform + if self.assetWriter.canAdd(videoInput) { + self.assetWriter.add(videoInput) + self.videoInput = videoInput + } else { + failed = true + } + } else { + failed = true + } + } + + if failed { + print("error") + return + } + + if self.assetWriter.status == .unknown { + if sampleBuffer.presentationTimestamp < self.recordingStartSampleTime { + return + } + if !self.assetWriter.startWriting() { + if let error = self.assetWriter.error { + self.transitionToFailedStatus(error: .avError(error)) + return + } + } + + self.assetWriter.startSession(atSourceTime: presentationTime) + self.recordingStartSampleTime = presentationTime + self.lastVideoSampleTime = presentationTime + } + + if self.assetWriter.status == .writing { + if self.recordingStopSampleTime != .invalid && sampleBuffer.presentationTimestamp > self.recordingStopSampleTime { + self.hasAllVideoBuffers = true + self.maybeFinish() + return + } + + if let videoInput = self.videoInput, videoInput.isReadyForMoreMediaData { + if videoInput.append(sampleBuffer) { + self.lastVideoSampleTime = presentationTime + let startTime = self.recordingStartSampleTime + let duration = presentationTime - startTime + self._duration = duration + } else { + print("error") + } + if !self.tryAppendingPendingAudioBuffers() { + self.transitionToFailedStatus(error: .generic) + } + } + } + } + } + + public func appendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) { + if let _ = self.hasError() { + return + } + + guard let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer), CMFormatDescriptionGetMediaType(formatDescription) == kCMMediaType_Audio else { + return + } + + self.queue.async { + guard !self.stopped && self.error.with({ $0 }) == nil else { + return + } + + var failed = false + if self.audioInput == nil { + var audioSettings = self.configuration.audioSettings + if let currentAudioStreamBasicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) { + audioSettings[AVSampleRateKey] = currentAudioStreamBasicDescription.pointee.mSampleRate + audioSettings[AVNumberOfChannelsKey] = currentAudioStreamBasicDescription.pointee.mChannelsPerFrame + } + + var audioChannelLayoutSize: Int = 0 + let currentChannelLayout = CMAudioFormatDescriptionGetChannelLayout(formatDescription, sizeOut: &audioChannelLayoutSize) + let currentChannelLayoutData: Data + if let currentChannelLayout = currentChannelLayout, audioChannelLayoutSize > 0 { + currentChannelLayoutData = Data(bytes: currentChannelLayout, count: audioChannelLayoutSize) + } else { + currentChannelLayoutData = Data() + } + audioSettings[AVChannelLayoutKey] = currentChannelLayoutData + + if self.assetWriter.canApply(outputSettings: audioSettings, forMediaType: .audio) { + let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings, sourceFormatHint: formatDescription) + audioInput.expectsMediaDataInRealTime = true + if self.assetWriter.canAdd(audioInput) { + self.assetWriter.add(audioInput) + self.audioInput = audioInput + } else { + failed = true + } + } else { + failed = true + } + } + + if failed { + print("error") + return + } + + if self.assetWriter.status == .writing { + if sampleBuffer.presentationTimestamp < self.recordingStartSampleTime { + return + } + if self.recordingStopSampleTime != .invalid && sampleBuffer.presentationTimestamp > self.recordingStopSampleTime { + self.hasAllAudioBuffers = true + self.maybeFinish() + return + } + var result = false + if self.tryAppendingPendingAudioBuffers() { + if self.tryAppendingAudioSampleBuffer(sampleBuffer) { + result = true + } + } + if !result { + self.transitionToFailedStatus(error: .generic) + } + } + } + } + + public func cancelRecording(completion: @escaping () -> Void) { + self.queue.async { + if self.stopped { + DispatchQueue.main.async { + completion() + } + return + } + self.stopped = true + self.pendingAudioSampleBuffers = [] + if self.assetWriter.status == .writing { + self.assetWriter.cancelWriting() + } + let fileManager = FileManager() + try? fileManager.removeItem(at: self.url) + DispatchQueue.main.async { + completion() + } + } + } + + public var isRecording: Bool { + self.queue.sync { !(self.hasAllVideoBuffers && self.hasAllAudioBuffers) } + } + + public func stopRecording() { + self.queue.async { + self.recordingStopSampleTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + } + } + + public func maybeFinish() { + self.queue.async { + guard self.hasAllVideoBuffers && self.hasAllVideoBuffers else { + return + } + self.stopped = true + self.finish() + } + } + + public func finish() { + self.queue.async { + let completion = self.completion + if self.recordingStopSampleTime == .invalid { + DispatchQueue.main.async { + completion(false) + } + return + } + + if let _ = self.error.with({ $0 }) { + DispatchQueue.main.async { + completion(false) + } + return + } + + if !self.tryAppendingPendingAudioBuffers() { + DispatchQueue.main.async { + completion(false) + } + return + } + + if self.assetWriter.status == .writing { + self.assetWriter.finishWriting { + if let _ = self.assetWriter.error { + DispatchQueue.main.async { + completion(false) + } + } else { + DispatchQueue.main.async { + completion(true) + } + } + } + } else if let _ = self.assetWriter.error { + DispatchQueue.main.async { + completion(true) + } + } else { + DispatchQueue.main.async { + completion(true) + } + } + } + } + + private func tryAppendingPendingAudioBuffers() -> Bool { + dispatchPrecondition(condition: .onQueue(self.queue)) + guard self.pendingAudioSampleBuffers.count > 0 else { + return true + } + + var result = true + let (sampleBuffersToAppend, pendingSampleBuffers) = self.pendingAudioSampleBuffers.stableGroup(using: { $0.endTime <= self.lastVideoSampleTime }) + for sampleBuffer in sampleBuffersToAppend { + if !self.internalAppendAudioSampleBuffer(sampleBuffer) { + result = false + break + } + } + self.pendingAudioSampleBuffers = pendingSampleBuffers + return result + } + + private func tryAppendingAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) -> Bool { + dispatchPrecondition(condition: .onQueue(self.queue)) + + var result = true + if sampleBuffer.endTime > self.lastVideoSampleTime { + self.pendingAudioSampleBuffers.append(sampleBuffer) + } else { + result = self.internalAppendAudioSampleBuffer(sampleBuffer) + } + return result + } + + private func internalAppendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) -> Bool { + if let audioInput = self.audioInput, audioInput.isReadyForMoreMediaData { + if !audioInput.append(sampleBuffer) { + if let _ = self.assetWriter.error { + return false + } + } + } else { + + } + return true + } + + private func transitionToFailedStatus(error: RecorderError) { + let _ = self.error.modify({ _ in return error }) + } +} + +private extension Sequence { + func stableGroup(using predicate: (Element) throws -> Bool) rethrows -> ([Element], [Element]) { + var trueGroup: [Element] = [] + var falseGroup: [Element] = [] + for element in self { + if try predicate(element) { + trueGroup.append(element) + } else { + falseGroup.append(element) + } + } + return (trueGroup, falseGroup) + } +} + +public final class VideoRecorder { + var duration: Double? { + return self.impl.duration.seconds + } + enum Result { enum Error { case generic } case success + case initError(Error) case writeError(Error) case finishError(Error) } + struct Configuration { + var videoSettings: [String: Any] + var audioSettings: [String: Any] + + init(videoSettings: [String: Any], audioSettings: [String: Any]) { + self.videoSettings = videoSettings + self.audioSettings = audioSettings + } + + var hasAudio: Bool { + return !self.audioSettings.isEmpty + } + } + + private let impl: VideoRecorderImpl + fileprivate let configuration: Configuration + fileprivate let videoTransform: CGAffineTransform + fileprivate let fileUrl: URL private let completion: (Result) -> Void - private let queue = Queue() - private var assetWriter: AVAssetWriter? + public var isRecording: Bool { + return self.impl.isRecording + } - private var videoInput: AVAssetWriterInput? - private var audioInput: AVAssetWriterInput? - - private let preset: MediaPreset - private let videoTransform: CGAffineTransform - private let fileUrl: URL - - private (set) var isRecording = false - private (set) var isStopping = false - private var finishedWriting = false - - private var captureStartTimestamp: Double? - private var firstVideoTimestamp: CMTime? - private var lastVideoTimestamp: CMTime? - private var lastAudioTimestamp: CMTime? - - private var pendingAudioBuffers: [CMSampleBuffer] = [] - - init(preset: MediaPreset, videoTransform: CGAffineTransform, fileUrl: URL, completion: @escaping (Result) -> Void) { - self.preset = preset + init?(configuration: Configuration, videoTransform: CGAffineTransform, fileUrl: URL, completion: @escaping (Result) -> Void) { + self.configuration = configuration self.videoTransform = videoTransform self.fileUrl = fileUrl self.completion = completion - } - - func start() { - self.queue.async { - guard self.assetWriter == nil else { - return - } - - self.captureStartTimestamp = CFAbsoluteTimeGetCurrent() - - guard let assetWriter = try? AVAssetWriter(url: self.fileUrl, fileType: .mp4) else { - return - } - - let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: self.preset.videoSettings) - videoInput.expectsMediaDataInRealTime = true - videoInput.transform = self.videoTransform - if assetWriter.canAdd(videoInput) { - assetWriter.add(videoInput) - } - - let audioInput: AVAssetWriterInput? - if self.preset.hasAudio { - audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: self.preset.audioSettings) - audioInput!.expectsMediaDataInRealTime = true - if assetWriter.canAdd(audioInput!) { - assetWriter.add(audioInput!) - } - } else { - audioInput = nil - } - - self.assetWriter = assetWriter - self.videoInput = videoInput - self.audioInput = audioInput - - self.isRecording = true - - //assetWriter.startWriting() - } - } - - func stop() { - self.queue.async { - guard let captureStartTimestamp = self.captureStartTimestamp, abs(CFAbsoluteTimeGetCurrent() - captureStartTimestamp) > 0.5 else { - return - } - - self.isStopping = true - - if self.audioInput == nil { - self.finish() - } - } - } - - private func finish() { - guard let assetWriter = self.assetWriter else { - return - } - self.queue.async { - self.isRecording = false - self.isStopping = false - - assetWriter.finishWriting { - self.finishedWriting = true - - if case .completed = assetWriter.status { + guard let impl = VideoRecorderImpl(configuration: configuration, videoTransform: videoTransform, fileUrl: fileUrl) else { + completion(.initError(.generic)) + return nil + } + self.impl = impl + impl.completion = { [weak self] success in + if let self { + if success { self.completion(.success) } else { self.completion(.finishError(.generic)) @@ -132,76 +442,25 @@ final class VideoRecorder { } } - func appendVideo(sampleBuffer: CMSampleBuffer) { - self.queue.async { - guard let assetWriter = self.assetWriter, let videoInput = self.videoInput, (self.isRecording || self.isStopping) && !self.finishedWriting else { - return - } - let timestamp = sampleBuffer.presentationTimestamp - if let startTimestamp = self.captureStartTimestamp, timestamp.seconds < startTimestamp { - return - } - - switch assetWriter.status { - case .unknown: - break - case .writing: - if self.firstVideoTimestamp == nil { - self.firstVideoTimestamp = timestamp - assetWriter.startSession(atSourceTime: timestamp) - } - while !videoInput.isReadyForMoreMediaData { - RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) - } - - if videoInput.append(sampleBuffer) { - self.lastVideoTimestamp = timestamp - } - - if self.audioInput != nil && self.isStopping, let lastVideoTimestamp = self.lastAudioTimestamp, let lastAudioTimestamp = self.lastAudioTimestamp, lastVideoTimestamp >= lastAudioTimestamp { - self.finish() - } - case .failed: - self.isRecording = false - self.completion(.writeError(.generic)) - default: - break - } - } + func start() { + self.impl.start() } - func appendAudio(sampleBuffer: CMSampleBuffer) { - self.queue.async { - guard let _ = self.assetWriter, let audioInput = self.audioInput, !self.isStopping && !self.finishedWriting else { - return - } - let timestamp = sampleBuffer.presentationTimestamp - - if let _ = self.firstVideoTimestamp { - if !self.pendingAudioBuffers.isEmpty { - for buffer in self.pendingAudioBuffers { - audioInput.append(buffer) - } - self.pendingAudioBuffers.removeAll() - } - - while !audioInput.isReadyForMoreMediaData { - RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) - } - - if audioInput.append(sampleBuffer) { - self.lastAudioTimestamp = timestamp - } - } else { - self.pendingAudioBuffers.append(sampleBuffer) - } - } + func stop() { + self.impl.stopRecording() } - var duration: Double? { - guard let firstTimestamp = self.firstVideoTimestamp, let lastTimestamp = self.lastVideoTimestamp else { - return nil + func appendSampleBuffer(_ sampleBuffer: CMSampleBuffer) { + guard let formatDescriptor = CMSampleBufferGetFormatDescription(sampleBuffer) else { + return + } + let type = CMFormatDescriptionGetMediaType(formatDescriptor) + if type == kCMMediaType_Video { + self.impl.appendVideoSampleBuffer(sampleBuffer) + } else if type == kCMMediaType_Audio { + if self.configuration.hasAudio { + self.impl.appendAudioSampleBuffer(sampleBuffer) + } } - return (lastTimestamp - firstTimestamp).seconds } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index d58748b840..b7598ed110 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -840,7 +840,10 @@ public class CameraScreen: ViewController { let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) self.effectivePreviewView.addGestureRecognizer(panGestureRecognizer) - self.camera.focus(at: CGPoint(x: 0.5, y: 0.5)) + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.effectivePreviewView.addGestureRecognizer(tapGestureRecognizer) + + self.camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true) self.camera.startCapture() } @@ -856,8 +859,6 @@ public class CameraScreen: ViewController { } } - private var previewInitialPosition: CGPoint? - private var controlsInitialPosition: CGPoint? @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { guard let controller = self.controller else { return @@ -865,8 +866,7 @@ public class CameraScreen: ViewController { let translation = gestureRecognizer.translation(in: gestureRecognizer.view) switch gestureRecognizer.state { case .began: - self.previewInitialPosition = self.previewContainerView.center - self.controlsInitialPosition = self.componentHost.view?.center + break case .changed: if !"".isEmpty { @@ -888,6 +888,15 @@ public class CameraScreen: ViewController { break } } + + @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + guard let previewView = self.simplePreviewView else { + return + } + let location = gestureRecognizer.location(in: previewView) + let point = previewView.cameraPoint(for: location) + self.camera.focus(at: point, autoFocus: false) + } func animateIn() { self.backgroundView.alpha = 0.0 diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 85ab785831..8f60ae7242 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1160,7 +1160,7 @@ public final class MediaEditorScreen: ViewController { rotateGestureRecognizer.delegate = self self.previewContainerView.addGestureRecognizer(rotateGestureRecognizer) - let tapGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) self.previewContainerView.addGestureRecognizer(tapGestureRecognizer) self.interaction = DrawingToolsInteraction(