Files
ichmagmaus111andClaude Opus 5.5 534cabe35b bug fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:55:08 +01:00

1386 lines
60 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import UIKit
import AVFoundation
import CoreMedia
import CoreVideo
import SwiftSignalKit
import DeviceModel
import Camera
import os.log
// GHOST: Ghostgram round video settings and frame processor (masks)
import GGCameraPipeline
private let cameraCaptureLog = OSLog(subsystem: "org.telegram.CameraNeo", category: "Capture")
private enum CaptureInitializationStatus {
case pending
case ready
case failed(CameraRecordingError)
}
final class CameraNeo: NSObject, CameraProtocol {
private let configuration: Camera.Configuration
private let useMultiCam: Bool
private let roundVideoMode: NeoCameraImpl.RoundVideoMode
private let log: ((String) -> Void)?
private let previewView: CameraPreviewView?
private let secondaryPreviewView: CameraPreviewView?
private let sessionQueue = DispatchQueue(label: "Camera.Session", qos: .userInitiated)
private let mediaQueue = DispatchQueue(label: "Camera.Media", qos: .userInitiated)
private let sessionQueueKey = DispatchSpecificKey<Void>()
private let mediaQueueKey = DispatchSpecificKey<Void>()
private let session: AVCaptureSession
private var wantsRunning = false
private var isConfigured = false
private var isInvalidated = false
private var backDevice: AVCaptureDevice?
private var frontDevice: AVCaptureDevice?
private var singleVideoInput: AVCaptureDeviceInput?
private var backVideoInput: AVCaptureDeviceInput?
private var frontVideoInput: AVCaptureDeviceInput?
private var audioInput: AVCaptureDeviceInput?
private var primaryVideoOutput: AVCaptureVideoDataOutput?
private var secondaryVideoOutput: AVCaptureVideoDataOutput?
private var audioOutput: AVCaptureAudioDataOutput?
private var stabilizationObservers: [NSKeyValueObservation] = []
private let secondaryVideoOutputIdentity = Atomic<ObjectIdentifier?>(value: nil)
private let audioOutputIdentity = Atomic<ObjectIdentifier?>(value: nil)
private var singleFramesEnabled = true
private var singleMediaPosition: Camera.Position
private var recordingOrientation: AVCaptureVideoOrientation = .portrait
private var frameProcessor: RoundVideoFrameProcessor?
private var recorder: VideoRecorder?
private let recordingActive = Atomic<Bool>(value: false)
private let recordingStopRequested = Atomic<Bool>(value: false)
private let recordingDuration = Atomic<Double>(value: 0.0)
private let singleCameraSwitchGeneration = Atomic<Int>(value: 0)
private let recordingCompletionPipe = ValuePipe<VideoCaptureResult>()
private let captureInitializationStatus = Atomic<CaptureInitializationStatus>(value: .pending)
private var mainVideoOutput: CameraVideoOutput?
private var additionalVideoOutput: CameraVideoOutput?
private let positionValue: Atomic<Camera.Position>
private let positionPromise: ValuePromise<Camera.Position>
private let flashModePromise = ValuePromise<Camera.FlashMode>(.off)
private let hasTorchPromise = ValuePromise<Bool>(false)
private let isFlashActivePromise = ValuePromise<Bool>(false)
private let transitionImagePromise = ValuePromise<UIImage?>(nil)
private let modeChangePromise = ValuePromise<Camera.ModeChange>(.none)
let metrics: Camera.Metrics
// GHOST: round video settings of this session (snapshot), the frame processor (masks) and the
// capture parameters derived from them; defaults are upstream's 640×480 at 30 fps
private let ggRecording: GGRoundVideoRecordingConfig
// GHOST: the zoom asked for since the last flip (`setZoomLevel`'s level, nil = 1×). A flip
// reconfigures the session after the request arrives, so the new device takes it from here.
private let ggRequestedZoomLevel = Atomic<CGFloat?>(value: nil)
private let ggFrameProcessor: GGCameraFrameProcessor?
private var ggFrameRate: Double = roundVideoCaptureFrameRate
private var ggFormatDescription: CMVideoFormatDescription?
init(
configuration: Camera.Configuration,
previewView: CameraPreviewView?,
secondaryPreviewView: CameraPreviewView?,
useMultiCam: Bool,
roundVideoMode: NeoCameraImpl.RoundVideoMode,
log: ((String) -> Void)?
) {
self.configuration = configuration
self.previewView = previewView
self.secondaryPreviewView = secondaryPreviewView
self.useMultiCam = useMultiCam
self.roundVideoMode = roundVideoMode
self.log = log
self.singleMediaPosition = configuration.position
self.positionValue = Atomic<Camera.Position>(value: configuration.position)
self.positionPromise = ValuePromise<Camera.Position>(configuration.position)
self.metrics = Camera.Metrics(model: DeviceModel.current)
// GHOST: see `ggRecording`
self.ggRecording = configuration.isRoundVideo ? GGRoundVideoConfig.recording() : .standard
self.ggFrameProcessor = configuration.frameProcessor
if useMultiCam, #available(iOS 13.0, *) {
self.session = AVCaptureMultiCamSession()
} else {
self.session = AVCaptureSession()
}
super.init()
self.session.usesApplicationAudioSession = true
self.session.automaticallyConfiguresApplicationAudioSession = false
self.sessionQueue.setSpecific(key: self.sessionQueueKey, value: ())
self.mediaQueue.setSpecific(key: self.mediaQueueKey, value: ())
self.previewView?.setSession(self.session, automaticallyConnect: !useMultiCam)
if useMultiCam {
self.secondaryPreviewView?.setSession(self.session, automaticallyConnect: false)
}
NotificationCenter.default.addObserver(
self,
selector: #selector(self.sessionRuntimeError(_:)),
name: .AVCaptureSessionRuntimeError,
object: self.session
)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.sessionInterruptionEnded(_:)),
name: .AVCaptureSessionInterruptionEnded,
object: self.session
)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.sessionDidStartRunning(_:)),
name: .AVCaptureSessionDidStartRunning,
object: self.session
)
self.sessionQueue.async { [weak self] in
self?.configureSession()
}
}
deinit {
NotificationCenter.default.removeObserver(self)
let cleanUpSession = {
self.primaryVideoOutput?.setSampleBufferDelegate(nil, queue: nil)
self.secondaryVideoOutput?.setSampleBufferDelegate(nil, queue: nil)
self.audioOutput?.setSampleBufferDelegate(nil, queue: nil)
if self.session.isRunning {
self.session.stopRunning()
}
self.tearDownSession()
}
if DispatchQueue.getSpecific(key: self.sessionQueueKey) != nil {
cleanUpSession()
} else {
self.sessionQueue.sync(execute: cleanUpSession)
}
if DispatchQueue.getSpecific(key: self.mediaQueueKey) != nil {
self.recorder?.cancel()
} else {
self.mediaQueue.sync {
self.recorder?.cancel()
}
}
}
func startCapture() {
self.sessionQueue.async { [weak self] in
guard let self, !self.isInvalidated else {
return
}
self.wantsRunning = true
if !self.isConfigured {
self.configureSession()
}
if self.isConfigured && !self.session.isRunning {
self.session.startRunning()
}
}
}
func stopCapture(invalidate: Bool) {
self.sessionQueue.async { [weak self] in
guard let self else {
return
}
self.wantsRunning = false
if self.session.isRunning {
self.session.stopRunning()
}
if invalidate {
self.isInvalidated = true
self.tearDownSession()
DispatchQueue.main.async { [weak self] in
self?.previewView?.invalidate()
self?.secondaryPreviewView?.invalidate()
}
}
}
if invalidate {
self.mediaQueue.async { [weak self] in
guard let self else {
return
}
self.recorder?.cancel()
self.recorder = nil
let _ = self.recordingActive.modify { _ in false }
let _ = self.recordingStopRequested.modify { _ in false }
}
}
}
var position: Signal<Camera.Position, NoError> {
return self.positionPromise.get()
}
func togglePosition() {
let currentPosition = self.positionValue.with { $0 }
self.setPosition(currentPosition == .front ? .back : .front)
}
func setPosition(_ position: Camera.Position) {
let previousPosition = self.positionValue.swap(position)
guard previousPosition != position else {
return
}
let _ = self.ggRequestedZoomLevel.swap(nil) // GHOST: the new camera starts at 1× unless asked otherwise
let switchGeneration = self.useMultiCam ? 0 : self.singleCameraSwitchGeneration.modify { $0 + 1 }
self.modeChangePromise.set(.position)
self.positionPromise.set(position)
self.mediaQueue.async { [weak self] in
guard let self else {
return
}
if !self.useMultiCam {
self.recorder?.beginSingleCameraSwitch(
generation: switchGeneration,
from: previousPosition,
to: position
)
}
self.recorder?.beginPositionChange(to: position)
if !self.useMultiCam {
self.singleFramesEnabled = false
}
}
self.sessionQueue.async { [weak self] in
guard let self, self.isConfigured, !self.isInvalidated else {
return
}
if previousPosition == .back, position == .front, let backDevice = self.backDevice {
self.withLockedDevice(backDevice) { device in
if device.isTorchModeSupported(.off) {
device.torchMode = .off
}
}
}
if self.useMultiCam {
self.updateActiveDeviceState()
} else {
self.switchSingleCamera(to: position, generation: switchGeneration)
}
self.sessionQueue.asyncAfter(deadline: .now() + 0.2) { [weak self] in
self?.modeChangePromise.set(.none)
}
}
}
func setDualCameraEnabled(_ enabled: Bool) {
}
func takePhoto() -> Signal<PhotoCaptureResult, NoError> {
return .complete()
}
func startRecording() -> Signal<CameraRecordingData, CameraRecordingError> {
guard self.configuration.isRoundVideo else {
return .complete()
}
if case let .failed(error) = self.captureInitializationStatus.with({ $0 }) {
return .fail(error)
}
var wasActive = false
let _ = self.recordingActive.modify { current in
wasActive = current
return true
}
guard !wasActive else {
return .complete()
}
let _ = self.recordingStopRequested.modify { _ in false }
let outputUrl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("mp4")
let orientation: AVCaptureVideoOrientation
if DeviceModel.current.isIpad {
orientation = self.previewView?.captureOrientation ?? .portrait
} else {
orientation = .portrait
}
let _ = self.recordingDuration.modify { _ in 0.0 }
self.transitionImagePromise.set(nil)
return Signal { [weak self] subscriber in
guard let self else {
subscriber.putCompletion()
return EmptyDisposable
}
let subscriberActive = Atomic<Bool>(value: true)
let recordingPrepared = Atomic<Bool>(value: false)
let beginRecording: (RoundVideoDecorationResources?) -> Void = { [weak self] decorationResources in
guard let self else {
return
}
self.mediaQueue.async { [weak self] in
guard let self else {
return
}
guard subscriberActive.with({ $0 }), self.recordingActive.with({ $0 }) else {
let _ = self.recordingActive.modify { _ in false }
let _ = self.recordingStopRequested.modify { _ in false }
return
}
if case let .failed(error) = self.captureInitializationStatus.with({ $0 }) {
let _ = self.recordingActive.modify { _ in false }
let _ = self.recordingStopRequested.modify { _ in false }
if subscriberActive.swap(false) {
subscriber.putError(error)
}
return
}
let processor: RoundVideoFrameProcessor
if let current = self.frameProcessor {
processor = current
} else if let decorationResources, let current = RoundVideoFrameProcessor(mediaQueue: self.mediaQueue, decorationResources: decorationResources, mode: self.roundVideoMode, outputSide: self.ggOutputSide /* GHOST: round video resolution */) {
self.frameProcessor = current
processor = current
} else {
guard let current = RoundVideoFrameProcessor(mediaQueue: self.mediaQueue, decorationResources: nil, mode: self.roundVideoMode, outputSide: self.ggOutputSide /* GHOST: round video resolution */) else {
let _ = self.recordingActive.modify { _ in false }
let _ = self.recordingStopRequested.modify { _ in false }
if subscriberActive.swap(false) {
subscriber.putError(.videoRecorderInitializationError)
}
return
}
self.frameProcessor = current
processor = current
}
self.recordingOrientation = orientation
let recorder = VideoRecorder(
mediaQueue: self.mediaQueue,
processor: processor,
url: outputUrl,
hasAudio: self.ggCapturesAudio, // GHOST: «record without sound»
initialPosition: self.positionValue.with { $0 },
// GHOST: bitrate, codec and watermark from the round video settings
ggRecording: self.ggRecording,
ggWatermark: GGRoundVideoConfig.watermark(side: processor.outputSide),
mode: self.roundVideoMode,
usesMultiCam: self.useMultiCam,
log: self.log,
durationUpdated: { [weak self] duration in
guard let self else {
return
}
let _ = self.recordingDuration.modify { _ in duration }
},
transitionImageUpdated: { [weak self] image in
self?.transitionImagePromise.set(image)
},
runtimeFailure: { error in
let wasActive = subscriberActive.swap(false)
if wasActive {
subscriber.putError(error)
}
},
completion: { [weak self] result in
guard let self else {
return
}
self.recorder = nil
let _ = self.recordingActive.modify { _ in false }
let _ = self.recordingStopRequested.modify { _ in false }
switch result {
case let .success(url, thumbnail, duration, positionChangeTimestamps):
self.recordingCompletionPipe.putNext(.finished(
main: VideoCaptureResult.Result(
path: url.path,
thumbnail: thumbnail ?? UIImage(),
isMirrored: false,
dimensions: CGSize(width: processor.outputSide, height: processor.outputSide) // GHOST: round video resolution
),
additional: nil,
duration: duration,
positionChangeTimestamps: positionChangeTimestamps.map { ($0.0 == .front, $0.1) },
captureTimestamp: CACurrentMediaTime()
))
case .failed:
self.recordingCompletionPipe.putNext(.failed)
}
}
)
self.recorder = recorder
let _ = recordingPrepared.modify { _ in true }
if self.recordingStopRequested.with({ $0 }) {
recorder.stop()
}
}
}
RoundVideoDecorationProvider.shared.prepare(completion: beginRecording)
let timer = SwiftSignalKit.Timer(timeout: 0.09, repeat: true, completion: { [weak self] in
guard let self, subscriberActive.with({ $0 }), recordingPrepared.with({ $0 }) else {
return
}
subscriber.putNext(CameraRecordingData(
duration: self.recordingDuration.with { $0 },
filePath: outputUrl.path
))
}, queue: Queue.mainQueue())
timer.start()
return ActionDisposable {
let _ = subscriberActive.modify { _ in false }
timer.invalidate()
}
}
}
func stopRecording() -> Signal<VideoCaptureResult, NoError> {
guard self.recordingActive.with({ $0 }) else {
return .complete()
}
let _ = self.recordingStopRequested.modify { _ in true }
self.mediaQueue.async { [weak self] in
self?.recorder?.stop()
}
return self.recordingCompletionPipe.signal()
|> take(1)
}
func focus(at point: CGPoint, autoFocus: Bool) {
self.sessionQueue.async { [weak self] in
guard let device = self?.activeDevice else {
return
}
self?.withLockedDevice(device) { device in
let focusMode: AVCaptureDevice.FocusMode = autoFocus ? .continuousAutoFocus : .autoFocus
let exposureMode: AVCaptureDevice.ExposureMode = autoFocus ? .continuousAutoExposure : .autoExpose
if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(focusMode) {
device.focusPointOfInterest = point
device.focusMode = focusMode
}
if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(exposureMode) {
device.exposurePointOfInterest = point
device.exposureMode = exposureMode
}
device.isSubjectAreaChangeMonitoringEnabled = true
}
}
}
func setFps(_ fps: Double) {
self.sessionQueue.async { [weak self] in
guard let self, !self.isInvalidated else {
return
}
self.session.beginConfiguration()
if self.useMultiCam {
if let backDevice = self.backDevice, let backVideoInput = self.backVideoInput {
self.configureFrameRate(fps, device: backDevice, input: backVideoInput)
}
if let frontDevice = self.frontDevice, let frontVideoInput = self.frontVideoInput {
self.configureFrameRate(fps, device: frontDevice, input: frontVideoInput)
}
} else if let activeDevice = self.activeDevice, let singleVideoInput = self.singleVideoInput {
self.configureFrameRate(fps, device: activeDevice, input: singleVideoInput)
}
self.session.commitConfiguration()
self.configureVideoStabilization()
}
}
func setFlashMode(_ flashMode: Camera.FlashMode) {
self.flashModePromise.set(flashMode)
}
func setZoomLevel(_ zoomLevel: CGFloat) {
let _ = self.ggRequestedZoomLevel.swap(zoomLevel) // GHOST: survives a flip that is still reconfiguring
self.updateZoom { [weak self] device in
return (self?.ggNeutralZoomFactor(device) ?? 1.0) * (1.0 + zoomLevel) // GHOST: relative to 1× (the ultra-wide camera makes 0.5× possible)
}
}
func setZoomDelta(_ zoomDelta: CGFloat) {
let _ = self.ggRequestedZoomLevel.swap(nil) // GHOST: a pinch replaces the requested zoom
self.updateZoom { device in
return device.videoZoomFactor * zoomDelta
}
}
func rampZoom(_ zoomLevel: CGFloat, rate: CGFloat) {
let _ = self.ggRequestedZoomLevel.swap(zoomLevel - 1.0) // GHOST: the ramp's target is the requested zoom (1.0 = 1×)
self.sessionQueue.async { [weak self] in
guard let self, let device = self.activeDevice else {
return
}
self.withLockedDevice(device) { device in
device.ramp(toVideoZoomFactor: self.clampedZoom(zoomLevel * self.ggNeutralZoomFactor(device), device: device), withRate: Float(rate)) // GHOST: 1.0 = 1×, not the ultra-wide 0.5×
}
}
}
func setTorchActive(_ active: Bool) {
self.sessionQueue.async { [weak self] in
guard let self, let device = self.activeDevice else {
return
}
var effectiveActive = false
self.withLockedDevice(device) { device in
let mode: AVCaptureDevice.TorchMode = active ? .on : .off
if device.isTorchModeSupported(mode) {
device.torchMode = mode
effectiveActive = active
}
}
self.isFlashActivePromise.set(effectiveActive)
}
}
var hasTorch: Signal<Bool, NoError> {
return self.hasTorchPromise.get()
}
var isFlashActive: Signal<Bool, NoError> {
return self.isFlashActivePromise.get()
}
var flashMode: Signal<Camera.FlashMode, NoError> {
return self.flashModePromise.get()
}
func setMainVideoOutput(_ output: CameraVideoOutput?) {
self.mediaQueue.async { [weak self] in
self?.mainVideoOutput = output
}
}
func setAdditionalVideoOutput(_ output: CameraVideoOutput?) {
self.mediaQueue.async { [weak self] in
self?.additionalVideoOutput = output
}
}
func attachSimplePreviewView(_ view: CameraSimplePreviewView) {
}
var detectedCodes: Signal<[CameraCode], NoError> {
return .never()
}
var audioLevel: Signal<Float, NoError> {
return .never()
}
var transitionImage: Signal<UIImage?, NoError> {
return self.transitionImagePromise.get()
}
var modeChange: Signal<Camera.ModeChange, NoError> {
return self.modeChangePromise.get()
}
private var activeDevice: AVCaptureDevice? {
if self.positionValue.with({ $0 }) == .front {
return self.frontDevice
} else {
return self.backDevice
}
}
private func configureSession() {
dispatchPrecondition(condition: .onQueue(self.sessionQueue))
guard !self.isConfigured, !self.isInvalidated else {
return
}
self.session.beginConfiguration()
self.session.sessionPreset = .inputPriority
var videoConfigured: Bool
if self.useMultiCam {
videoConfigured = self.configureMultiCamVideo()
} else {
videoConfigured = self.configureSingleCameraVideo(position: self.positionValue.with { $0 }, addOutput: true)
}
let audioConfigured = !self.ggCapturesAudio || self.configureAudio() // GHOST: no microphone when recording without sound
self.session.commitConfiguration()
self.configureVideoStabilization()
if videoConfigured && self.useMultiCam {
videoConfigured = self.configureSupportedMultiCamCosts()
}
self.logCaptureState(reason: "configured")
self.isConfigured = videoConfigured && audioConfigured
let initializationStatus: CaptureInitializationStatus
if !videoConfigured {
initializationStatus = .failed(.videoRecorderInitializationError)
} else if !audioConfigured {
initializationStatus = .failed(.audioInitializationError)
} else {
initializationStatus = .ready
}
let _ = self.captureInitializationStatus.modify { _ in initializationStatus }
if !self.isConfigured {
self.tearDownSession()
if case let .failed(error) = initializationStatus {
self.mediaQueue.async { [weak self] in
self?.recorder?.failInitialization(error)
}
}
}
self.updateActiveDeviceState()
DispatchQueue.main.async { [weak self] in
self?.previewView?.updateOrientation()
self?.secondaryPreviewView?.updateOrientation()
}
}
private func configureSingleCameraVideo(position: Camera.Position, addOutput: Bool) -> Bool {
guard var device = self.makeVideoDevice(position: position) else {
return false
}
if !self.configureCaptureFormat(device: device) {
// GHOST: the ultra-wide virtual camera has no matching format: the plain wide camera, as upstream
guard self.ggIsUltraWideVirtualDevice(device), let fallback = self.makeWideVideoDevice(position: position), self.configureCaptureFormat(device: fallback) else {
return false
}
device = fallback
}
let videoInput: AVCaptureDeviceInput
do {
videoInput = try AVCaptureDeviceInput(device: device)
} catch {
return false
}
guard self.session.canAddInput(videoInput) else {
return false
}
self.session.addInput(videoInput)
self.configureFrameRate(self.ggFrameRate, device: device, input: videoInput) // GHOST: 30 or 60 fps
self.ggApplyNeutralZoom(device) // GHOST: the ultra-wide virtual camera at 1×, also after a flip
if addOutput {
guard let output = self.makeVideoOutput(), self.session.canAddOutput(output) else {
self.session.removeInput(videoInput)
return false
}
self.session.addOutput(output)
self.primaryVideoOutput = output
}
self.singleVideoInput = videoInput
if position == .front {
self.frontDevice = device
} else {
self.backDevice = device
}
return true
}
private func configureMultiCamVideo() -> Bool {
guard #available(iOS 13.0, *),
let multiCamSession = self.session as? AVCaptureMultiCamSession,
let backDevice = self.makeVideoDevice(position: .back),
let frontDevice = self.makeVideoDevice(position: .front) else {
return false
}
guard self.configureCaptureFormat(device: backDevice), self.configureCaptureFormat(device: frontDevice) else {
return false
}
guard let backInput = try? AVCaptureDeviceInput(device: backDevice),
let frontInput = try? AVCaptureDeviceInput(device: frontDevice),
multiCamSession.canAddInput(backInput),
multiCamSession.canAddInput(frontInput),
let backOutput = self.makeVideoOutput(),
let frontOutput = self.makeVideoOutput(),
multiCamSession.canAddOutput(backOutput),
multiCamSession.canAddOutput(frontOutput) else {
return false
}
multiCamSession.addInputWithNoConnections(backInput)
multiCamSession.addInputWithNoConnections(frontInput)
// GHOST: 30 or 60 fps (60 records with one camera, so this stays 30 here)
self.configureFrameRate(self.ggFrameRate, device: backDevice, input: backInput)
self.configureFrameRate(self.ggFrameRate, device: frontDevice, input: frontInput)
multiCamSession.addOutputWithNoConnections(backOutput)
multiCamSession.addOutputWithNoConnections(frontOutput)
guard self.connectVideo(
input: backInput,
output: backOutput,
previewView: self.previewView,
mirrored: false
), self.connectVideo(
input: frontInput,
output: frontOutput,
previewView: self.secondaryPreviewView,
mirrored: true
) else {
return false
}
self.backDevice = backDevice
self.frontDevice = frontDevice
self.backVideoInput = backInput
self.frontVideoInput = frontInput
self.primaryVideoOutput = backOutput
self.secondaryVideoOutput = frontOutput
let _ = self.secondaryVideoOutputIdentity.modify { _ in ObjectIdentifier(frontOutput) }
return true
}
@available(iOS 13.0, *)
private func connectVideo(
input: AVCaptureDeviceInput,
output: AVCaptureVideoDataOutput,
previewView: CameraPreviewView?,
mirrored: Bool
) -> Bool {
let videoPorts = input.ports.filter { $0.mediaType == .video }
guard !videoPorts.isEmpty else {
return false
}
let outputConnection = AVCaptureConnection(inputPorts: videoPorts, output: output)
outputConnection.automaticallyAdjustsVideoMirroring = false
if outputConnection.isVideoMirroringSupported {
outputConnection.isVideoMirrored = false
}
guard self.session.canAddConnection(outputConnection) else {
return false
}
self.session.addConnection(outputConnection)
if let previewView, let port = videoPorts.first {
let previewConnection = AVCaptureConnection(inputPort: port, videoPreviewLayer: previewView.videoPreviewLayer)
previewConnection.automaticallyAdjustsVideoMirroring = false
if previewConnection.isVideoMirroringSupported {
previewConnection.isVideoMirrored = mirrored
}
guard self.session.canAddConnection(previewConnection) else {
return false
}
self.session.addConnection(previewConnection)
}
return true
}
private func configureAudio() -> Bool {
guard let audioDevice = AVCaptureDevice.default(for: .audio),
let input = try? AVCaptureDeviceInput(device: audioDevice) else {
return false
}
let output = AVCaptureAudioDataOutput()
output.setSampleBufferDelegate(self, queue: self.mediaQueue)
if self.useMultiCam, #available(iOS 13.0, *), let multiCamSession = self.session as? AVCaptureMultiCamSession {
guard multiCamSession.canAddInput(input), multiCamSession.canAddOutput(output) else {
return false
}
multiCamSession.addInputWithNoConnections(input)
multiCamSession.addOutputWithNoConnections(output)
let audioPorts = input.ports.filter { $0.mediaType == .audio }
guard !audioPorts.isEmpty else {
return false
}
let connection = AVCaptureConnection(inputPorts: audioPorts, output: output)
guard multiCamSession.canAddConnection(connection) else {
return false
}
multiCamSession.addConnection(connection)
} else {
guard self.session.canAddInput(input), self.session.canAddOutput(output) else {
return false
}
self.session.addInput(input)
self.session.addOutput(output)
}
self.audioInput = input
self.audioOutput = output
let _ = self.audioOutputIdentity.modify { _ in ObjectIdentifier(output) }
return true
}
private func makeVideoOutput() -> AVCaptureVideoDataOutput? {
let output = AVCaptureVideoDataOutput()
let formats = output.availableVideoPixelFormatTypes
let pixelFormat: OSType
if formats.contains(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
} else if formats.contains(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
} else {
return nil
}
output.videoSettings = [
kCVPixelBufferPixelFormatTypeKey as String: pixelFormat
]
output.alwaysDiscardsLateVideoFrames = true
output.setSampleBufferDelegate(self, queue: self.mediaQueue)
return output
}
private func makeVideoDevice(position: Camera.Position) -> AVCaptureDevice? {
// GHOST: Ghostgram's 0.5× zoom of round videos: the back camera as the virtual camera with the
// ultra-wide lens (one camera only; the dual-camera session keeps the plain one)
if self.ggRecording.usesUltraWideCamera, position == .back, !self.useMultiCam, #available(iOS 13.0, *) {
if let device = AVCaptureDevice.default(.builtInTripleCamera, for: .video, position: .back) {
return device
}
if let device = AVCaptureDevice.default(.builtInDualWideCamera, for: .video, position: .back) {
return device
}
}
return self.makeWideVideoDevice(position: position)
}
// GHOST: upstream's own choice, also the fallback of the ultra-wide virtual camera
private func makeWideVideoDevice(position: Camera.Position) -> AVCaptureDevice? {
if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position) {
return device
}
return AVCaptureDevice.DiscoverySession(
deviceTypes: [.builtInWideAngleCamera, .builtInTrueDepthCamera],
mediaType: .video,
position: position
).devices.first
}
private func configureCaptureFormat(device: AVCaptureDevice) -> Bool {
// GHOST: capture format and frame rate from the round video settings when they differ from
// Telegram's: the first option that the device supports wins, the last one is 640×480 at 30 fps
if self.ggUsesOwnCaptureFormat {
for option in self.ggCaptureOptions {
if let format = self.ggCaptureFormat(device: device, width: option.width, height: option.height, frameRate: option.frameRate), self.applyCaptureFormat(format, device: device) {
self.ggFrameRate = option.frameRate
return true
}
}
return false
}
guard let selectedFormat = roundVideoCaptureFormat(for: device, requiresMultiCam: self.useMultiCam, mode: self.roundVideoMode) else {
return false
}
return self.applyCaptureFormat(selectedFormat, device: device)
}
private func applyCaptureFormat(_ format: AVCaptureDevice.Format, device: AVCaptureDevice) -> Bool {
do {
try device.lockForConfiguration()
defer { device.unlockForConfiguration() }
device.activeFormat = format
if device.isLowLightBoostSupported {
device.automaticallyEnablesLowLightBoostWhenAvailable = true
}
if device.isExposureModeSupported(.continuousAutoExposure) {
device.exposureMode = .continuousAutoExposure
}
// GHOST: the ultra-wide virtual camera starts at 1× (its own factor 1 is the 0.5× lens)
if self.ggIsUltraWideVirtualDevice(device) {
device.videoZoomFactor = self.clampedZoom(self.ggNeutralZoomFactor(device), device: device)
}
return true
} catch {
return false
}
}
private func configureSupportedMultiCamCosts() -> Bool {
dispatchPrecondition(condition: .onQueue(self.sessionQueue))
guard #available(iOS 13.0, *), let session = self.session as? AVCaptureMultiCamSession,
let backDevice = self.backDevice, let frontDevice = self.frontDevice,
let backInput = self.backVideoInput, let frontInput = self.frontVideoInput else {
return false
}
let initialProfile: RoundVideoCaptureProfile = self.roundVideoMode == .vga ? .vga : .high
for maximumProfile in RoundVideoCaptureProfile.allCases where maximumProfile.rawValue >= initialProfile.rawValue {
if maximumProfile != initialProfile {
guard let backFormat = roundVideoCaptureFormat(for: backDevice, requiresMultiCam: true, mode: self.roundVideoMode, maximumProfile: maximumProfile),
let frontFormat = roundVideoCaptureFormat(for: frontDevice, requiresMultiCam: true, mode: self.roundVideoMode, maximumProfile: maximumProfile) else {
continue
}
session.beginConfiguration()
let backConfigured = self.applyCaptureFormat(backFormat, device: backDevice)
let frontConfigured = self.applyCaptureFormat(frontFormat, device: frontDevice)
self.configureFrameRate(roundVideoCaptureFrameRate, device: backDevice, input: backInput)
self.configureFrameRate(roundVideoCaptureFrameRate, device: frontDevice, input: frontInput)
session.commitConfiguration()
guard backConfigured && frontConfigured else {
return false
}
self.configureVideoStabilization()
}
self.logCaptureState(reason: "MultiCam budget")
if roundVideoCaptureCostsAreSupported(hardwareCost: session.hardwareCost, systemPressureCost: session.systemPressureCost) {
return true
}
}
return false
}
private func configureFrameRate(_ fps: Double, device: AVCaptureDevice, input: AVCaptureDeviceInput) {
guard fps.isFinite, fps > 0.0 else {
return
}
guard let range = device.activeFormat.videoSupportedFrameRateRanges.first(where: { $0.minFrameRate <= fps && $0.maxFrameRate >= fps }) else {
return
}
let effectiveFps = min(range.maxFrameRate, max(range.minFrameRate, fps))
let duration = CMTime(seconds: 1.0 / effectiveFps, preferredTimescale: 60_000)
self.withLockedDevice(device) { device in
if #available(iOS 18.0, *), device.activeFormat.isAutoVideoFrameRateSupported, device.isAutoVideoFrameRateEnabled {
device.isAutoVideoFrameRateEnabled = false
}
device.activeVideoMinFrameDuration = duration
device.activeVideoMaxFrameDuration = duration
}
input.videoMinFrameDurationOverride = duration
}
private func configureVideoStabilization() {
dispatchPrecondition(condition: .onQueue(self.sessionQueue))
self.stabilizationObservers.removeAll()
for (output, device) in [
(self.primaryVideoOutput, self.useMultiCam ? self.backDevice : self.activeDevice),
(self.secondaryVideoOutput, self.frontDevice)
] {
guard let connection = output?.connection(with: .video), let device else {
continue
}
let supportsStandard = connection.isVideoStabilizationSupported && device.activeFormat.isVideoStabilizationModeSupported(.standard)
connection.preferredVideoStabilizationMode = self.ggStabilizationMode(device: device, upstream: supportsStandard ? .standard : .off) // GHOST: stabilization setting
self.stabilizationObservers.append(connection.observe(\.activeVideoStabilizationMode, options: [.new]) { [weak self, weak connection] _, _ in
self?.sessionQueue.async { [weak self, weak connection] in
guard let self, let connection,
connection === self.primaryVideoOutput?.connection(with: .video) || connection === self.secondaryVideoOutput?.connection(with: .video) else {
return
}
self.logCaptureState(reason: "stabilization changed")
}
})
}
}
private func logCaptureState(reason: String) {
dispatchPrecondition(condition: .onQueue(self.sessionQueue))
for (output, device, preview) in [
(self.primaryVideoOutput, self.useMultiCam ? self.backDevice : self.activeDevice, self.previewView),
(self.secondaryVideoOutput, self.frontDevice, self.secondaryPreviewView)
] {
guard let connection = output?.connection(with: .video), let device else {
continue
}
let format = device.activeFormat
let previewMode = preview?.videoPreviewLayer.connection?.activeVideoStabilizationMode.rawValue ?? -1
let message = "\(reason): mode=\(self.roundVideoMode.rawValue), position=\(device.position.rawValue), format=\(format), frameDuration=\(device.activeVideoMinFrameDuration.seconds), standardSupported=\(format.isVideoStabilizationModeSupported(.standard)), requested=\(connection.preferredVideoStabilizationMode.rawValue), active=\(connection.activeVideoStabilizationMode.rawValue), previewActive=\(previewMode), zoom=\(device.videoZoomFactor), upscaleThreshold=\(format.videoZoomFactorUpscaleThreshold)"
os_log("%{public}@", log: cameraCaptureLog, type: .info, message)
self.log?(message)
}
if #available(iOS 13.0, *), let session = self.session as? AVCaptureMultiCamSession {
let message = "\(reason): hardwareCost=\(session.hardwareCost), systemPressureCost=\(session.systemPressureCost)"
os_log("%{public}@", log: cameraCaptureLog, type: .info, message)
self.log?(message)
}
}
private func switchSingleCamera(to position: Camera.Position, generation: Int) {
dispatchPrecondition(condition: .onQueue(self.sessionQueue))
self.session.beginConfiguration()
if let singleVideoInput = self.singleVideoInput {
self.session.removeInput(singleVideoInput)
}
self.singleVideoInput = nil
let configured = self.configureSingleCameraVideo(position: position, addOutput: false)
self.session.commitConfiguration()
self.configureVideoStabilization()
self.logCaptureState(reason: "camera switched")
if !configured {
let error = CameraRecordingError.videoRecorderInitializationError
let _ = self.captureInitializationStatus.modify { _ in .failed(error) }
if self.session.isRunning {
self.session.stopRunning()
}
self.tearDownSession()
self.mediaQueue.async { [weak self] in
self?.recorder?.noteSingleCameraSwitchCommitted(generation: generation)
self?.singleFramesEnabled = false
self?.recorder?.failInitialization(error)
}
self.updateActiveDeviceState()
return
}
self.mediaQueue.async { [weak self] in
self?.recorder?.noteSingleCameraSwitchCommitted(generation: generation)
self?.singleMediaPosition = position
self?.singleFramesEnabled = true
}
self.updateActiveDeviceState()
}
private func updateActiveDeviceState() {
guard let device = self.activeDevice else {
self.hasTorchPromise.set(false)
self.isFlashActivePromise.set(false)
return
}
self.hasTorchPromise.set(device.hasTorch && device.isTorchAvailable)
self.isFlashActivePromise.set(device.torchMode == .on)
}
private func updateZoom(_ value: @escaping (AVCaptureDevice) -> CGFloat) {
self.sessionQueue.async { [weak self] in
guard let self, let device = self.activeDevice else {
return
}
self.withLockedDevice(device) { device in
device.videoZoomFactor = self.clampedZoom(value(device), device: device)
}
}
}
// GHOST: a virtual camera that starts with the ultra-wide lens (zoom factor 1 = 0.5×)
private func ggIsUltraWideVirtualDevice(_ device: AVCaptureDevice) -> Bool {
if #available(iOS 13.0, *) {
return device.isVirtualDevice && device.constituentDevices.first?.deviceType == .builtInUltraWideCamera
}
return false
}
// GHOST: the ultra-wide virtual camera at 1× once its input is in the session. The factor set with
// the format did not survive adding the input: a flip to the back camera showed the 0.5× lens while
// the zoom slider said 1×.
private func ggApplyNeutralZoom(_ device: AVCaptureDevice) {
guard self.ggIsUltraWideVirtualDevice(device) else {
return
}
// GHOST: the zoom asked for while the flip was reconfiguring («0,5× на задней камере»): applied
// to the new device here, or the camera showed 1× under a slider at 0.5×.
let requestedLevel = self.ggRequestedZoomLevel.with { $0 } ?? 0.0
self.withLockedDevice(device) { device in
device.videoZoomFactor = self.clampedZoom(self.ggNeutralZoomFactor(device) * (1.0 + requestedLevel), device: device) // GHOST: see requestedLevel
}
}
// GHOST: the zoom factor of 1× — the switch-over to the wide lens on the ultra-wide virtual camera, 1 elsewhere
private func ggNeutralZoomFactor(_ device: AVCaptureDevice) -> CGFloat {
if #available(iOS 13.0, *), self.ggIsUltraWideVirtualDevice(device), let wideIndex = device.constituentDevices.firstIndex(where: { $0.deviceType == .builtInWideAngleCamera }), wideIndex > 0, wideIndex - 1 < device.virtualDeviceSwitchOverVideoZoomFactors.count {
return CGFloat(device.virtualDeviceSwitchOverVideoZoomFactors[wideIndex - 1].doubleValue)
}
return 1.0
}
private func clampedZoom(_ value: CGFloat, device: AVCaptureDevice) -> CGFloat {
let minimum = max(1.0, device.minAvailableVideoZoomFactor)
let maximum = max(minimum, device.maxAvailableVideoZoomFactor)
return min(maximum, max(minimum, value))
}
private func withLockedDevice(_ device: AVCaptureDevice, _ update: (AVCaptureDevice) -> Void) {
do {
try device.lockForConfiguration()
update(device)
device.unlockForConfiguration()
} catch {
}
}
private func tearDownSession() {
dispatchPrecondition(condition: .onQueue(self.sessionQueue))
self.stabilizationObservers.removeAll()
self.primaryVideoOutput?.setSampleBufferDelegate(nil, queue: nil)
self.secondaryVideoOutput?.setSampleBufferDelegate(nil, queue: nil)
self.audioOutput?.setSampleBufferDelegate(nil, queue: nil)
self.session.beginConfiguration()
for output in self.session.outputs {
self.session.removeOutput(output)
}
for input in self.session.inputs {
self.session.removeInput(input)
}
self.session.commitConfiguration()
self.primaryVideoOutput = nil
self.secondaryVideoOutput = nil
self.audioOutput = nil
let _ = self.secondaryVideoOutputIdentity.modify { _ in nil }
let _ = self.audioOutputIdentity.modify { _ in nil }
self.singleVideoInput = nil
self.backVideoInput = nil
self.frontVideoInput = nil
self.audioInput = nil
self.backDevice = nil
self.frontDevice = nil
self.isConfigured = false
}
@objc private func sessionDidStartRunning(_ notification: Notification) {
self.sessionQueue.async { [weak self] in
self?.logCaptureState(reason: "started")
}
}
@objc private func sessionRuntimeError(_ notification: Notification) {
// GHOST: two guards — any error other than a media-services reset goes to ggSessionDidFail (upstream returned)
guard let errorValue = notification.userInfo?[AVCaptureSessionErrorKey] as? NSError else {
return
}
guard AVError(_nsError: errorValue).code == .mediaServicesWereReset else {
self.ggSessionDidFail() // GHOST: see ggSessionDidFail
return
}
self.sessionQueue.async { [weak self] in
guard let self, !self.isInvalidated else {
return
}
let shouldRestart = self.wantsRunning
let _ = self.captureInitializationStatus.modify { _ in .pending }
if self.session.isRunning {
self.session.stopRunning()
}
self.tearDownSession()
if shouldRestart {
self.configureSession()
if self.isConfigured {
self.session.startRunning()
}
}
}
}
@objc private func sessionInterruptionEnded(_ notification: Notification) {
self.sessionQueue.async { [weak self] in
guard let self, self.wantsRunning, self.isConfigured, !self.isInvalidated, !self.session.isRunning else {
return
}
self.session.startRunning()
}
}
}
// GHOST: Ghostgram round video settings and frame processor
extension CameraNeo {
/// Any other runtime error stops the session for good (a dual-camera session over its hardware
/// budget, a device it can not run…). Upstream ignored it, so the recording waited for frames that
/// never came; now it fails, and the round camera screen goes on with the legacy camera
/// (`VideoMessageCameraScreen.Node.ggFallBackToLegacyCamera`). Only once the session has stopped,
/// and only before anything was recorded: a clip already under way is finished by «stop» as before.
func ggSessionDidFail() {
self.sessionQueue.async { [weak self] in
guard let self, !self.isInvalidated, !self.session.isRunning, self.recordingDuration.with({ $0 }) == 0.0 else {
return
}
let error = CameraRecordingError.videoRecorderInitializationError
let _ = self.captureInitializationStatus.modify { _ in .failed(error) }
self.mediaQueue.async { [weak self] in
self?.recorder?.failInitialization(error)
}
}
}
private var ggCapturesAudio: Bool {
return self.configuration.audio && !self.ggRecording.muteAudio
}
private var ggOutputSide: Int {
return self.ggRecording.outputSide(defaultSide: RoundVideoFrameProcessor.outputWidth)
}
/// The round video settings ask for a capture of their own (a side or a frame rate other than
/// Telegram's); otherwise the capture is Telegram's (`roundVideoCaptureFormat`).
private var ggUsesOwnCaptureFormat: Bool {
return self.ggRecording.side != nil || Double(self.ggRecording.frameRate) != roundVideoCaptureFrameRate
}
/// The format of one capture option: `width` 0 = any 4:3 format at least `height` high (up to
/// 1440 — the photo-sized ones are slow and heavy), the smallest matching one first.
private func ggCaptureFormat(device: AVCaptureDevice, width: Int32, height: Int32, frameRate: Double) -> AVCaptureDevice.Format? {
let candidates = device.formats.filter { format in
if self.useMultiCam, #available(iOS 13.0, *), !format.isMultiCamSupported {
return false
}
let dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription)
let mediaSubtype = CMFormatDescriptionGetMediaSubType(format.formatDescription)
let matchesSize = width == 0 ? (dimensions.width * 3 == dimensions.height * 4 && dimensions.height >= height && dimensions.height <= 1440) : (dimensions.width == width && dimensions.height == height)
guard matchesSize,
(mediaSubtype == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange || mediaSubtype == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) else {
return false
}
return format.videoSupportedFrameRateRanges.contains(where: {
$0.minFrameRate <= frameRate && $0.maxFrameRate >= frameRate
})
}
return candidates.min { lhs, rhs in
let lhsHeight = CMVideoFormatDescriptionGetDimensions(lhs.formatDescription).height
let rhsHeight = CMVideoFormatDescriptionGetDimensions(rhs.formatDescription).height
if lhsHeight != rhsHeight {
return lhsHeight < rhsHeight
}
let lhsMaximumFps = lhs.videoSupportedFrameRateRanges.map(\.maxFrameRate).max() ?? 0.0
let rhsMaximumFps = rhs.videoSupportedFrameRateRanges.map(\.maxFrameRate).max() ?? 0.0
return lhsMaximumFps < rhsMaximumFps
}
}
private var ggCaptureOptions: [(width: Int32, height: Int32, frameRate: Double)] {
var options: [(width: Int32, height: Int32, frameRate: Double)] = []
let frameRate = Double(self.ggRecording.frameRate)
if self.ggRecording.needsHighResolutionCapture(defaultSide: RoundVideoFrameProcessor.outputWidth) {
// A 4:3 format first (width 0 = «4:3, at least this high», see configureCaptureFormat): the
// square of a round video keeps the sensor's whole short side, as upstream's 640×480 does.
// From 1280×720 (16:9) it lost a quarter of the view — the front camera looked zoomed in.
let side = Int32(self.ggRecording.outputSide(defaultSide: RoundVideoFrameProcessor.outputWidth))
options.append((0, side, frameRate))
options.append((1280, 720, frameRate))
if frameRate != roundVideoCaptureFrameRate {
options.append((0, side, roundVideoCaptureFrameRate))
options.append((1280, 720, roundVideoCaptureFrameRate))
}
}
if frameRate != roundVideoCaptureFrameRate {
options.append((640, 480, frameRate))
}
options.append((640, 480, roundVideoCaptureFrameRate))
return options
}
/// Stabilization from the settings; `upstream` is Telegram's own choice for this format.
private func ggStabilizationMode(device: AVCaptureDevice, upstream: AVCaptureVideoStabilizationMode) -> AVCaptureVideoStabilizationMode {
switch self.ggRecording.stabilization {
case .standard:
return upstream
case .off:
return .off
case .cinematic:
if device.activeFormat.isVideoStabilizationModeSupported(.cinematic) {
return .cinematic
}
return upstream
}
}
private func ggProcessedSampleBuffer(_ sampleBuffer: CMSampleBuffer, position: Camera.Position) -> CMSampleBuffer {
guard let processor = self.ggFrameProcessor, position == self.positionValue.with({ $0 }), let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return sampleBuffer
}
let quarterTurns: Int
switch self.recordingOrientation {
case .landscapeLeft:
quarterTurns = position == .front ? 0 : 2
case .landscapeRight:
quarterTurns = position == .front ? 2 : 0
case .portraitUpsideDown:
quarterTurns = 3
default:
quarterTurns = 1
}
let info = GGCameraFrameInfo(
facing: position == .front ? .front : .back,
timestamp: CMSampleBufferGetPresentationTimeStamp(sampleBuffer),
orientation: GGCameraFrameInfo.orientation(quarterTurns: quarterTurns, mirrored: position == .front)
)
guard let processed = processor.process(pixelBuffer, info: info) else {
return sampleBuffer
}
// One format description is reused while the buffers match it (no allocation per frame).
if let current = self.ggFormatDescription, CMVideoFormatDescriptionMatchesImageBuffer(current, imageBuffer: processed) {
} else {
var formatDescription: CMVideoFormatDescription?
guard CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: processed, formatDescriptionOut: &formatDescription) == noErr, let formatDescription else {
return sampleBuffer
}
self.ggFormatDescription = formatDescription
}
guard let formatDescription = self.ggFormatDescription else {
return sampleBuffer
}
var timing = CMSampleTimingInfo(
duration: CMSampleBufferGetDuration(sampleBuffer),
presentationTimeStamp: CMSampleBufferGetPresentationTimeStamp(sampleBuffer),
decodeTimeStamp: CMSampleBufferGetDecodeTimeStamp(sampleBuffer)
)
var result: CMSampleBuffer?
guard CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: processed, formatDescription: formatDescription, sampleTiming: &timing, sampleBufferOut: &result) == noErr, let result else {
return sampleBuffer
}
return result
}
}
extension CameraNeo: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
dispatchPrecondition(condition: .onQueue(self.mediaQueue))
let position: Camera.Position
if self.useMultiCam {
position = self.secondaryVideoOutputIdentity.with({ $0 }) == ObjectIdentifier(output) ? .front : .back
} else {
position = self.singleMediaPosition
}
self.recorder?.noteDroppedVideoSampleBuffer(sampleBuffer, position: position)
}
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
dispatchPrecondition(condition: .onQueue(self.mediaQueue))
let outputIdentity = ObjectIdentifier(output)
if self.audioOutputIdentity.with({ $0 }) == outputIdentity {
self.recorder?.appendAudioSampleBuffer(sampleBuffer)
return
}
let position: Camera.Position
if self.useMultiCam {
if self.secondaryVideoOutputIdentity.with({ $0 }) == outputIdentity {
position = .front
} else {
position = .back
}
} else {
position = self.singleMediaPosition
}
if !self.useMultiCam, !self.singleFramesEnabled {
return
}
// GHOST: Ghostgram frame processor (masks): once per frame of the visible camera, the same
// processed frame goes to the recorder and to the live preview
let sampleBuffer = self.ggProcessedSampleBuffer(sampleBuffer, position: position)
self.recorder?.appendVideoSampleBuffer(
sampleBuffer,
position: position,
orientation: self.recordingOrientation
)
if self.useMultiCam {
if position == .front {
self.additionalVideoOutput?.push(sampleBuffer, mirror: true)
} else {
self.mainVideoOutput?.push(sampleBuffer, mirror: false)
}
} else {
self.mainVideoOutput?.push(sampleBuffer, mirror: position == .front)
}
}
}