diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 6790ba59ec..d879212c91 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -146,7 +146,7 @@ private final class CameraContext { transform = CGAffineTransformTranslate(transform, 0.0, -size.height) ciImage = ciImage.transformed(by: transform) } - ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: Camera.isDualCameraSupported(forRoundVideo: true) ? 100.0 : 40.0).cropped(to: CGRect(origin: .zero, size: size)) + ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: Camera.isDualCameraSupported(forRoundVideo: true) ? 60.0 : 40.0).cropped(to: CGRect(origin: .zero, size: size)) if let cgImage = self.ciContext.createCGImage(ciImage, from: ciImage.extent) { let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) if front { @@ -189,6 +189,7 @@ private final class CameraContext { deinit { Logger.shared.log("CameraContext", "deinit") + NotificationCenter.default.removeObserver(self) } private var isSessionRunning = false @@ -202,7 +203,7 @@ private final class CameraContext { } func stopCapture(invalidate: Bool = false) { - Logger.shared.log("CameraContext", "startCapture(invalidate: \(invalidate))") + Logger.shared.log("CameraContext", "stopCapture(invalidate: \(invalidate))") if invalidate { self.mainDeviceContext?.device.resetZoom() @@ -212,6 +213,7 @@ private final class CameraContext { } self.session.session.stopRunning() + self.isSessionRunning = false } func focus(at point: CGPoint, autoFocus: Bool) { @@ -228,7 +230,7 @@ private final class CameraContext { } func setFps(_ fps: Float64) { - self.mainDeviceContext?.device.fps = fps + self.mainDeviceContext?.device.setFps(fps) } private var modeChange: Camera.ModeChange = .none { @@ -1170,6 +1172,7 @@ public struct CameraRecordingData { } public enum CameraRecordingError { + case videoRecorderInitializationError case audioInitializationError } diff --git a/submodules/Camera/Sources/CameraDevice.swift b/submodules/Camera/Sources/CameraDevice.swift index 6778c2dff5..e07d634d41 100644 --- a/submodules/Camera/Sources/CameraDevice.swift +++ b/submodules/Camera/Sources/CameraDevice.swift @@ -141,10 +141,12 @@ final class CameraDevice { Logger.shared.log("Camera", "No format selected") } + #if DEBUG Logger.shared.log("Camera", "Available formats:") for format in device.formats { Logger.shared.log("Camera", format.description) } + #endif if let targetFPS = device.actualFPS(maxFramerate) { device.activeVideoMinFrameDuration = targetFPS.duration @@ -180,18 +182,16 @@ final class CameraDevice { self.setFocusPoint(CGPoint(x: 0.5, y: 0.5), focusMode: .continuousAutoFocus, exposureMode: .continuousAutoExposure, monitorSubjectAreaChange: false) } - var fps: Double = defaultFPS { - didSet { - guard let device = self.videoDevice, let targetFPS = device.actualFPS(Double(self.fps)) else { - return - } - - self.fps = targetFPS.fps - - self.transaction(device) { device in - device.activeVideoMinFrameDuration = targetFPS.duration - device.activeVideoMaxFrameDuration = targetFPS.duration - } + private(set) var fps: Double = defaultFPS + + func setFps(_ fps: Double) { + guard let device = self.videoDevice, let targetFPS = device.actualFPS(Double(self.fps)) else { + return + } + self.fps = targetFPS.fps + self.transaction(device) { device in + device.activeVideoMinFrameDuration = targetFPS.duration + device.activeVideoMaxFrameDuration = targetFPS.duration } } diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index a1f0a3fb1c..4872e7635b 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -93,6 +93,11 @@ public struct CameraCode: Equatable { } final class CameraOutput: NSObject { + private struct RoundVideoFormatDescriptionCacheEntry { + let sourceFormatDescription: CMFormatDescription + let outputFormatDescription: CMFormatDescription + } + let exclusive: Bool let ciContext: CIContext let colorSpace: CGColorSpace @@ -111,13 +116,14 @@ final class CameraOutput: NSObject { private var roundVideoFilter: CameraRoundLegacyVideoFilter? private let semaphore = DispatchSemaphore(value: 1) + private var roundVideoFormatDescriptionCache: [RoundVideoFormatDescriptionCacheEntry] = [] private let videoQueue = DispatchQueue(label: "", qos: .userInitiated) private let audioQueue = DispatchQueue(label: "") private let metadataQueue = DispatchQueue(label: "") - private var photoCaptureRequests: [Int64: PhotoCaptureContext] = [:] + private var photoCaptureRequests = Atomic<[Int64: PhotoCaptureContext]>(value: [:]) private var videoRecorder: VideoRecorder? private var captureOrientation: AVCaptureVideoOrientation = .portrait @@ -268,8 +274,8 @@ final class CameraOutput: NSObject { return EmptyDisposable } subscriber.putNext(self.photoOutput.isFlashScene) - let observer = self.photoOutput.observe(\.isFlashScene, options: [.new], changeHandler: { device, _ in - subscriber.putNext(self.photoOutput.isFlashScene) + let observer = self.photoOutput.observe(\.isFlashScene, options: [.new], changeHandler: { output, _ in + subscriber.putNext(output.isFlashScene) }) return ActionDisposable { observer.invalidate() @@ -316,12 +322,20 @@ final class CameraOutput: NSObject { #else let uniqueId = settings.uniqueID let photoCapture = PhotoCaptureContext(ciContext: self.ciContext, settings: settings, orientation: orientation, mirror: mirror) - self.photoCaptureRequests[uniqueId] = photoCapture + let _ = self.photoCaptureRequests.modify { dict in + var dict = dict + dict[uniqueId] = photoCapture + return dict + } self.photoOutput.capturePhoto(with: settings, delegate: photoCapture) return photoCapture.signal |> afterDisposed { [weak self] in - self?.photoCaptureRequests.removeValue(forKey: uniqueId) + let _ = self?.photoCaptureRequests.modify { dict in + var dict = dict + dict.removeValue(forKey: uniqueId) + return dict + } } #endif } @@ -419,18 +433,21 @@ final class CameraOutput: NSObject { } } ) + guard let videoRecorder else { + return .fail(.videoRecorderInitializationError) + } - videoRecorder?.start() + videoRecorder.start() self.videoRecorder = videoRecorder if case .dualCamera = mode, let position { - videoRecorder?.markPositionChange(position: position, time: .zero) + videoRecorder.markPositionChange(position: position, time: .zero) } else if case .roundVideo = mode { additionalOutput?.masterOutput = self } return Signal { subscriber in - let timer = SwiftSignalKit.Timer(timeout: 0.033, repeat: true, completion: { [weak videoRecorder] in + let timer = SwiftSignalKit.Timer(timeout: 0.1, repeat: true, completion: { [weak videoRecorder] in let recordingData = CameraRecordingData(duration: videoRecorder?.duration ?? 0.0, filePath: outputFilePath) subscriber.putNext(recordingData) }, queue: Queue.mainQueue()) @@ -456,13 +473,54 @@ final class CameraOutput: NSObject { } var transitionImage: UIImage? { - return self.videoRecorder?.transitionImage + var result: UIImage? + self.videoQueue.sync { + result = self.videoRecorder?.transitionImage + } + return result } private weak var masterOutput: CameraOutput? private var lastSampleTimestamp: CMTime? + private func roundVideoFormatDescription(for sourceFormatDescription: CMFormatDescription) -> CMFormatDescription? { + if let entry = self.roundVideoFormatDescriptionCache.first(where: { CFEqual($0.sourceFormatDescription, sourceFormatDescription) }) { + return entry.outputFormatDescription + } + + guard let extensions = CMFormatDescriptionGetExtensions(sourceFormatDescription) as? [String: Any] else { + return nil + } + + let mediaSubType = CMFormatDescriptionGetMediaSubType(sourceFormatDescription) + var updatedExtensions = extensions + updatedExtensions["CVBytesPerRow"] = videoMessageDimensions.width * 4 + + var outputFormatDescription: CMFormatDescription? + let status = CMVideoFormatDescriptionCreate( + allocator: nil, + codecType: mediaSubType, + width: videoMessageDimensions.width, + height: videoMessageDimensions.height, + extensions: updatedExtensions as CFDictionary, + formatDescriptionOut: &outputFormatDescription + ) + guard status == noErr, let outputFormatDescription else { + return nil + } + + self.roundVideoFormatDescriptionCache.append(RoundVideoFormatDescriptionCacheEntry( + sourceFormatDescription: sourceFormatDescription, + outputFormatDescription: outputFormatDescription + )) + if self.roundVideoFormatDescriptionCache.count > 4 { + self.roundVideoFormatDescriptionCache.removeFirst(self.roundVideoFormatDescriptionCache.count - 4) + } + + return outputFormatDescription + } + private var needsCrossfadeTransition = false private var crossfadeTransitionStart: Double = 0.0 @@ -564,17 +622,11 @@ final class CameraOutput: NSObject { return nil } self.semaphore.wait() - - let mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription) - let extensions = CMFormatDescriptionGetExtensions(formatDescription) as! [String: Any] - - var updatedExtensions = extensions - updatedExtensions["CVBytesPerRow"] = videoMessageDimensions.width * 4 - - var newFormatDescription: CMFormatDescription? - var status = CMVideoFormatDescriptionCreate(allocator: nil, codecType: mediaSubType, width: videoMessageDimensions.width, height: videoMessageDimensions.height, extensions: updatedExtensions as CFDictionary, formatDescriptionOut: &newFormatDescription) - guard status == noErr, let newFormatDescription else { + defer { self.semaphore.signal() + } + + guard let newFormatDescription = self.roundVideoFormatDescription(for: formatDescription) else { return nil } @@ -585,12 +637,11 @@ final class CameraOutput: NSObject { filter = CameraRoundLegacyVideoFilter(ciContext: self.ciContext, colorSpace: self.colorSpace, simple: self.exclusive) self.roundVideoFilter = filter } - if !filter.isPrepared { + if !filter.isPrepared || filter.inputFormatDescription.map({ !CFEqual($0, newFormatDescription) }) ?? true { filter.prepare(with: newFormatDescription, outputRetainedBufferCountHint: 4) } guard let newPixelBuffer = filter.render(pixelBuffer: videoPixelBuffer, additional: additional, captureOrientation: self.captureOrientation, transitionFactor: transitionFactor) else { - self.semaphore.signal() return nil } @@ -603,7 +654,7 @@ final class CameraOutput: NSObject { } var newSampleBuffer: CMSampleBuffer? - status = CMSampleBufferCreateForImageBuffer( + let status = CMSampleBufferCreateForImageBuffer( allocator: kCFAllocatorDefault, imageBuffer: newPixelBuffer, dataReady: true, @@ -615,10 +666,8 @@ final class CameraOutput: NSObject { ) if status == noErr, let newSampleBuffer { - self.semaphore.signal() return newSampleBuffer } - self.semaphore.signal() return nil } @@ -640,18 +689,18 @@ extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureA guard CMSampleBufferDataIsReady(sampleBuffer) else { return } - - if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { - self.processSampleBuffer?(sampleBuffer, videoPixelBuffer, connection) - } else if sampleBuffer.type == kCMMediaType_Audio { - self.processAudioBuffer?(sampleBuffer) - } - + if let masterOutput = self.masterOutput { masterOutput.processVideoRecording(sampleBuffer, fromAdditionalOutput: true) } else { self.processVideoRecording(sampleBuffer, fromAdditionalOutput: false) } + + if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { + self.processSampleBuffer?(sampleBuffer, videoPixelBuffer, connection) + } else if sampleBuffer.type == kCMMediaType_Audio { + self.processAudioBuffer?(sampleBuffer) + } } func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { diff --git a/submodules/Camera/Sources/PhotoCaptureContext.swift b/submodules/Camera/Sources/PhotoCaptureContext.swift index c61fe73602..3e34fc5904 100644 --- a/submodules/Camera/Sources/PhotoCaptureContext.swift +++ b/submodules/Camera/Sources/PhotoCaptureContext.swift @@ -56,6 +56,7 @@ final class PhotoCaptureContext: NSObject, AVCapturePhotoCaptureDelegate { } else { guard let photoPixelBuffer = photo.pixelBuffer else { print("Error occurred while capturing photo: Missing pixel buffer (\(String(describing: error)))") + self.pipe.putNext(.failed) return } diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index a2d3a7cb83..5748938a75 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -204,9 +204,7 @@ private final class VideoRecorderImpl { let maxDate = Date(timeIntervalSinceNow: 0.05) RunLoop.current.run(until: maxDate) } - } - if let videoInput = self.videoInput { let time = CACurrentMediaTime() // if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime { // print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)") @@ -366,7 +364,7 @@ private final class VideoRecorderImpl { private func maybeFinish() { dispatchPrecondition(condition: .onQueue(self.queue)) - guard self.hasAllVideoBuffers && self.hasAllVideoBuffers && !self.stopped else { + guard self.hasAllVideoBuffers && (!self.configuration.hasAudio || self.hasAllAudioBuffers) && !self.stopped else { return } let _ = self._stopped.modify { _ in return true } diff --git a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m index a0bb1df327..f92214b2b1 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m +++ b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m @@ -138,6 +138,10 @@ - (void)createDismissViewIfNeeded { + if (_dismissView != nil) { + return; + } + UIView *parentView = [self _parentView]; _dismissView = [[UIView alloc] initWithFrame:parentView.bounds]; @@ -231,6 +235,12 @@ - (void)finishEditing { if ([self.inputPanel dismissInput]) { _editing = false; + + [UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + _dismissView.alpha = 0.0f; + } completion:^(BOOL finished) { + + }]; if (self.finishedWithCaption != nil) self.finishedWithCaption([_inputPanel caption]); diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift index 44e310c99e..1e3adc0e28 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift @@ -260,7 +260,11 @@ private func proxySettingsControllerEntries(theme: PresentationTheme, strings: P entries.append(.serversHeader(theme, strings.SocksProxySetup_SavedProxies)) entries.append(.addServer(theme, strings.SocksProxySetup_AddProxy, state.editing)) var index = 0 + var existingServers = Set() for server in proxySettings.servers { + if !existingServers.insert(server).inserted { + continue + } let status: ProxyServerStatus = statuses[server] ?? .checking let displayStatus: DisplayProxyServerStatus if proxySettings.enabled && server == proxySettings.activeServer { @@ -301,7 +305,7 @@ private func proxySettingsControllerEntries(theme: PresentationTheme, strings: P entries.append(.server(index, theme, strings, server, server == proxySettings.activeServer, displayStatus, ProxySettingsServerItemEditing(editable: true, editing: state.editing, revealed: state.revealedServer == server), proxySettings.enabled)) index += 1 } - if !proxySettings.servers.isEmpty { + if !existingServers.isEmpty { entries.append(.shareProxyList(theme, strings.SocksProxySetup_ShareProxyList)) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift index 62f826c99e..560bd3fce2 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift @@ -347,8 +347,11 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: } } } else { - settings.servers.append(proxyServerSettings) - if settings.servers.count == 1 { + let wasEmpty = settings.servers.isEmpty + if !settings.servers.contains(proxyServerSettings) { + settings.servers.append(proxyServerSettings) + } + if wasEmpty && settings.servers.count == 1 { settings.activeServer = proxyServerSettings } } @@ -388,4 +391,3 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: return controller } - diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift index c1d2974745..c06460e8dc 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift @@ -34,7 +34,7 @@ final class CameraVideoSource: VideoSource { let index = self.onUpdatedListeners.add(f) return ActionDisposable { [weak self] in - DispatchQueue.main.async { + Queue.mainQueue().async { guard let self else { return } @@ -252,7 +252,7 @@ final class LiveStreamMediaSource { let index = self.onVideoUpdatedListeners.add(f) return ActionDisposable { [weak self] in - DispatchQueue.main.async { + Queue.mainQueue().async { guard let self else { return } diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 4a74571527..aace91290f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -3427,6 +3427,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } } if let mediaDraftState = interfaceState.interfaceState.mediaDraftState, case .audio = mediaDraftState.contentType { + viewOnceIsVisible = true recordMoreIsVisible = true } @@ -3438,13 +3439,29 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg transition.updateSublayerTransformOffset(layer: self.clippingNode.layer, offset: CGPoint(x: 0.0, y: clippingDelta))*/ let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme) - let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: -152.0), size: viewOnceSize) + + var viewOnceButtonY: CGFloat = -105.0 + if isRecording { + if accessoryPanel == nil { + viewOnceButtonY -= 49.0 + } + } + + let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: viewOnceButtonY), size: viewOnceSize) self.viewOnceButton.bounds = CGRect(origin: .zero, size: viewOnceButtonFrame.size) transition.updatePosition(node: self.viewOnceButton, position: viewOnceButtonFrame.center) if self.viewOnceButton.alpha.isZero && viewOnceIsVisible { self.viewOnceButton.update(isSelected: self.viewOnce, animated: false) } + + transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0) + transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01) + if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil && interfaceState.sendPaidMessageStars == nil { + self.viewOnceButton.isHidden = false + } else { + self.viewOnceButton.isHidden = true + } let recordMoreSize = self.recordMoreButton.update(theme: interfaceState.theme) let recordMoreButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: -52.0), size: recordMoreSize) @@ -3455,14 +3472,6 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg self.recordMoreButton.update(isSelected: false, animated: false) } - transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0) - transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01) - if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil && interfaceState.sendPaidMessageStars == nil { - self.viewOnceButton.isHidden = false - } else { - self.viewOnceButton.isHidden = true - } - transition.updateAlpha(node: self.recordMoreButton, alpha: recordMoreIsVisible ? 1.0 : 0.0) transition.updateTransformScale(node: self.recordMoreButton, scale: recordMoreIsVisible ? 1.0 : 0.01)