NCCameraController.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. //
  2. // SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. //
  5. // Based on https://developer.apple.com/documentation/vision/applying_matte_effects_to_people_in_images_and_video
  6. import Foundation
  7. import Vision
  8. import CoreImage.CIFilterBuiltins
  9. @objc protocol NCCameraControllerDelegate {
  10. @objc func didDrawFirstFrameOnLocalView()
  11. }
  12. @objcMembers
  13. class NCCameraController: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, MTKViewDelegate {
  14. public weak var delegate: NCCameraControllerDelegate?
  15. // State
  16. private var backgroundBlurEnabled = NCUserDefaults.backgroundBlurEnabled()
  17. private var usingFrontCamera = true
  18. private var deviceOrientation: UIDeviceOrientation = UIDevice.current.orientation
  19. private var videoRotation: RTCVideoRotation = ._0
  20. private var firstLocalViewFrameDrawn = false
  21. // AVFoundation
  22. private var session: AVCaptureSession?
  23. // WebRTC
  24. private var videoSource: RTCVideoSource
  25. private var videoCapturer: RTCVideoCapturer
  26. private let framerateLimit = 30.0
  27. // Vision
  28. private let requestHandler = VNSequenceRequestHandler()
  29. private var segmentationRequest: VNGeneratePersonSegmentationRequest!
  30. // Metal
  31. private var metalDevice: MTLDevice!
  32. private var metalCommandQueue: MTLCommandQueue!
  33. public weak var localView: MTKView? {
  34. didSet {
  35. localView?.device = metalDevice
  36. localView?.isPaused = true
  37. localView?.enableSetNeedsDisplay = false
  38. localView?.delegate = self
  39. localView?.framebufferOnly = false
  40. localView?.contentMode = .scaleAspectFit
  41. }
  42. }
  43. // Core image
  44. private var context: CIContext!
  45. private var lastImage: CIImage?
  46. // MARK: - Init
  47. init(videoSource: RTCVideoSource, videoCapturer: RTCVideoCapturer) {
  48. self.videoSource = videoSource
  49. self.videoCapturer = videoCapturer
  50. super.init()
  51. initMetal()
  52. initVisionRequests()
  53. initAVCaptureSession()
  54. NotificationCenter.default.addObserver(self, selector: #selector(deviceOrientationDidChangeNotification), name: UIDevice.orientationDidChangeNotification, object: nil)
  55. self.updateVideoRotationBasedOnDeviceOrientation()
  56. }
  57. deinit {
  58. session?.stopRunning()
  59. }
  60. func initMetal() {
  61. metalDevice = MTLCreateSystemDefaultDevice()
  62. metalCommandQueue = metalDevice.makeCommandQueue()
  63. context = CIContext(mtlDevice: metalDevice)
  64. }
  65. func initVisionRequests() {
  66. // Create a request to segment a person from an image.
  67. segmentationRequest = VNGeneratePersonSegmentationRequest()
  68. segmentationRequest.qualityLevel = .balanced
  69. segmentationRequest.outputPixelFormat = kCVPixelFormatType_OneComponent8
  70. }
  71. func switchCamera() {
  72. var newInput: AVCaptureDeviceInput
  73. if self.usingFrontCamera {
  74. newInput = getBackCameraInput()
  75. } else {
  76. newInput = getFrontCameraInput()
  77. }
  78. if let firstInput = session?.inputs.first {
  79. session?.removeInput(firstInput)
  80. }
  81. // Stop and restart the session to prevent a weird glitch when rotating our local view
  82. self.session?.stopRunning()
  83. self.session?.addInput(newInput)
  84. // We need to set the orientation again, because otherweise after switching the video is turned
  85. self.session?.outputs.first?.connections.first?.videoOrientation = .portrait
  86. self.session?.startRunning()
  87. self.usingFrontCamera = !self.usingFrontCamera
  88. }
  89. // See ARDCaptureController from the WebRTC project
  90. func getVideoFormat(for device: AVCaptureDevice) -> AVCaptureDevice.Format? {
  91. let settings = NCSettingsController.sharedInstance().videoSettingsModel
  92. let formats = RTCCameraVideoCapturer.supportedFormats(for: device)
  93. let targetWidth = settings?.currentVideoResolutionWidthFromStore() ?? 0
  94. let targetHeight = settings?.currentVideoResolutionHeightFromStore() ?? 0
  95. var selectedFormat: AVCaptureDevice.Format?
  96. var currentDiff = INT_MAX
  97. for format in formats {
  98. let dimension = CMVideoFormatDescriptionGetDimensions(format.formatDescription)
  99. let diff = abs(targetWidth - dimension.width) + abs(targetHeight - dimension.height)
  100. if diff < currentDiff {
  101. selectedFormat = format
  102. currentDiff = diff
  103. }
  104. }
  105. return selectedFormat
  106. }
  107. // See ARDCaptureController from the WebRTC project
  108. func getVideoFps(for format: AVCaptureDevice.Format) -> Double {
  109. var maxFramerate = 0.0
  110. for fpsRange in format.videoSupportedFrameRateRanges {
  111. maxFramerate = fmax(maxFramerate, fpsRange.maxFrameRate)
  112. }
  113. return fmin(maxFramerate, framerateLimit)
  114. }
  115. func setFormat(for device: AVCaptureDevice) {
  116. if let format = getVideoFormat(for: device) {
  117. do {
  118. try device.lockForConfiguration()
  119. device.activeFormat = format
  120. let fps = Int32(getVideoFps(for: format))
  121. device.activeVideoMinFrameDuration = CMTimeMake(value: 1, timescale: fps)
  122. device.unlockForConfiguration()
  123. } catch {
  124. print("Could not lock configuration")
  125. }
  126. }
  127. }
  128. func getFrontCameraInput() -> AVCaptureDeviceInput {
  129. guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else {
  130. fatalError("Error getting AVCaptureDevice.")
  131. }
  132. self.setFormat(for: device)
  133. guard let input = try? AVCaptureDeviceInput(device: device) else {
  134. fatalError("Error getting AVCaptureDeviceInput")
  135. }
  136. return input
  137. }
  138. func getBackCameraInput() -> AVCaptureDeviceInput {
  139. guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
  140. fatalError("Error getting AVCaptureDevice.")
  141. }
  142. self.setFormat(for: device)
  143. guard let input = try? AVCaptureDeviceInput(device: device) else {
  144. fatalError("Error getting AVCaptureDeviceInput")
  145. }
  146. return input
  147. }
  148. func initAVCaptureSession() {
  149. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  150. guard let self = self else { return }
  151. self.session = AVCaptureSession()
  152. self.session?.sessionPreset = .inputPriority
  153. if self.usingFrontCamera {
  154. self.session?.addInput(self.getFrontCameraInput())
  155. } else {
  156. self.session?.addInput(self.getBackCameraInput())
  157. }
  158. let output = AVCaptureVideoDataOutput()
  159. output.alwaysDiscardsLateVideoFrames = true
  160. output.setSampleBufferDelegate(self, queue: .global(qos: .userInteractive))
  161. self.session?.addOutput(output)
  162. output.connections.first?.videoOrientation = .portrait
  163. self.session?.startRunning()
  164. }
  165. }
  166. public func stopAVCaptureSession() {
  167. self.session?.stopRunning()
  168. }
  169. // MARK: - Public switches
  170. public func enableBackgroundBlur(enable: Bool) {
  171. DispatchQueue.global(qos: .userInteractive).async {
  172. self.backgroundBlurEnabled = enable
  173. NCUserDefaults.setBackgroundBlurEnabled(enable)
  174. }
  175. }
  176. public func isBackgroundBlurEnabled() -> Bool {
  177. return self.backgroundBlurEnabled
  178. }
  179. // MARK: - Videoframe processing
  180. func blend(original frameImage: CIImage,
  181. mask maskPixelBuffer: CVPixelBuffer) -> CIImage? {
  182. // Create CIImage objects for the video frame and the segmentation mask.
  183. let originalImage = frameImage.oriented(.right)
  184. var maskImage = CIImage(cvPixelBuffer: maskPixelBuffer)
  185. // Scale the mask image to fit the bounds of the video frame.
  186. let scaleX = originalImage.extent.width / maskImage.extent.width
  187. let scaleY = originalImage.extent.height / maskImage.extent.height
  188. maskImage = maskImage.transformed(by: .init(scaleX: scaleX, y: scaleY))
  189. // Use "clampedToExtent()" to prevent black borders after applying the gaussian blur
  190. // Make sure to crop the image back afterwards to its original size, otherwise the result is disorted
  191. let backgroundImage = originalImage.clampedToExtent().applyingGaussianBlur(sigma: 8).cropped(to: originalImage.extent)
  192. // Blend the original, background, and mask images.
  193. let blendFilter = CIFilter.blendWithRedMask()
  194. blendFilter.inputImage = originalImage
  195. blendFilter.backgroundImage = backgroundImage
  196. blendFilter.maskImage = maskImage
  197. // Return the new blended image
  198. return blendFilter.outputImage?.oriented(.left)
  199. }
  200. func processVideoFrame(_ framePixelBuffer: CVPixelBuffer, _ sampleBuffer: CMSampleBuffer) {
  201. let pixelBuffer = framePixelBuffer
  202. var frameImage = CIImage(cvPixelBuffer: framePixelBuffer)
  203. if self.backgroundBlurEnabled {
  204. // Perform the requests on the pixel buffer that contains the video frame.
  205. try? requestHandler.perform([segmentationRequest],
  206. on: pixelBuffer,
  207. orientation: .right)
  208. // Get the pixel buffer that contains the mask image.
  209. guard let maskPixelBuffer = segmentationRequest.results?.first?.pixelBuffer else {
  210. return
  211. }
  212. // Process the images.
  213. if let newImage = blend(original: frameImage, mask: maskPixelBuffer) {
  214. context.render(newImage, to: pixelBuffer)
  215. frameImage = newImage
  216. }
  217. }
  218. self.lastImage = frameImage
  219. if let localView {
  220. localView.draw()
  221. if !self.firstLocalViewFrameDrawn {
  222. self.delegate?.didDrawFirstFrameOnLocalView()
  223. self.firstLocalViewFrameDrawn = true
  224. }
  225. }
  226. // Create the RTCVideoFrame
  227. let timeStampNs = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * Float64(NSEC_PER_SEC)
  228. let rtcpixelBuffer = RTCCVPixelBuffer(pixelBuffer: pixelBuffer)
  229. let videoFrame: RTCVideoFrame? = RTCVideoFrame(buffer: rtcpixelBuffer, rotation: videoRotation, timeStampNs: Int64(timeStampNs))
  230. if let videoFrame = videoFrame {
  231. self.videoSource.capturer(self.videoCapturer, didCapture: videoFrame)
  232. }
  233. }
  234. // MARK: - AVCaptureVideoDataOutputSampleBufferDelegate
  235. func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
  236. guard let pixelBuffer = sampleBuffer.imageBuffer else {
  237. return
  238. }
  239. self.processVideoFrame(pixelBuffer, sampleBuffer)
  240. }
  241. // MARK: - MTKViewDelegate
  242. func draw(in view: MTKView) {
  243. guard let commandBuffer = metalCommandQueue.makeCommandBuffer(),
  244. let currentDrawable = view.currentDrawable,
  245. let localView = localView,
  246. var ciImage = lastImage else {
  247. return
  248. }
  249. // Correctly rotate the local image
  250. if videoRotation == ._180 {
  251. ciImage = ciImage.oriented(.down)
  252. } else if videoRotation == ._90 {
  253. ciImage = ciImage.oriented(.right)
  254. } else if videoRotation == ._270 {
  255. ciImage = ciImage.oriented(.left)
  256. }
  257. // make sure the image is full screen
  258. let drawSize = localView.drawableSize
  259. let scaleX = drawSize.width / ciImage.extent.width
  260. let scaleY = drawSize.height / ciImage.extent.height
  261. var scale = scaleX
  262. // Make sure we use the smaller scale
  263. if scaleY < scaleX {
  264. scale = scaleY
  265. }
  266. // Make sure to scale by keeping the aspect ratio
  267. let newImage = ciImage.transformed(by: .init(scaleX: scale, y: scale))
  268. // render into the metal texture
  269. self.context.render(newImage,
  270. to: currentDrawable.texture,
  271. commandBuffer: commandBuffer,
  272. bounds: newImage.extent,
  273. colorSpace: CGColorSpaceCreateDeviceRGB())
  274. // register drawwable to command buffer
  275. commandBuffer.present(currentDrawable)
  276. commandBuffer.commit()
  277. }
  278. func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
  279. // Delegate method not implemented.
  280. }
  281. // MARK: - Notifications
  282. func deviceOrientationDidChangeNotification() {
  283. self.deviceOrientation = UIDevice.current.orientation
  284. self.updateVideoRotationBasedOnDeviceOrientation()
  285. }
  286. func updateVideoRotationBasedOnDeviceOrientation() {
  287. // Handle video rotation based on device orientation
  288. if deviceOrientation == .portrait {
  289. videoRotation = ._0
  290. } else if deviceOrientation == .portraitUpsideDown {
  291. videoRotation = ._180
  292. } else if deviceOrientation == .landscapeRight {
  293. videoRotation = usingFrontCamera ? ._270 : ._90
  294. } else if deviceOrientation == .landscapeLeft {
  295. videoRotation = usingFrontCamera ? ._90 : ._270
  296. }
  297. }
  298. }