Various fixes

This commit is contained in:
Ilya Laktyushin 2026-04-29 03:52:19 +02:00
parent 8a7daf3662
commit 7ab4685940
10 changed files with 140 additions and 64 deletions

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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) {

View file

@ -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
}

View file

@ -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 }

View file

@ -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]);

View file

@ -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<ProxyServerSettings>()
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))
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)