From cdb9487b4be9fc2b62b198e0275d2df95b28f4f6 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Thu, 6 Feb 2020 14:58:59 +1100 Subject: [PATCH] fix the issue that microphone is called when sending a photo --- .../ViewControllers/Photos/PhotoCapture.swift | 96 +++++++++++++++---- .../Photos/PhotoCaptureViewController.swift | 13 --- 2 files changed, 79 insertions(+), 30 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCapture.swift b/Signal/src/ViewControllers/Photos/PhotoCapture.swift index bc79eed01..ff10bf91e 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCapture.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCapture.swift @@ -33,11 +33,57 @@ class PhotoCapture: NSObject { return currentCaptureInput?.device } private(set) var desiredPosition: AVCaptureDevice.Position = .back + + let recordingAudioActivity = AudioActivity(audioDescription: "PhotoCapture", behavior: .playAndRecord) override init() { self.session = AVCaptureSession() self.captureOutput = CaptureOutput() } + + // MARK: - Dependencies + var audioSession: OWSAudioSession { + return Environment.shared.audioSession + } + + // MARK: - + var audioDeviceInput: AVCaptureDeviceInput? + func startAudioCapture() throws { + assertIsOnSessionQueue() + + guard audioSession.startAudioActivity(recordingAudioActivity) else { + throw PhotoCaptureError.assertionError(description: "unable to capture audio activity") + } + + self.session.beginConfiguration() + defer { self.session.commitConfiguration() } + + let audioDevice = AVCaptureDevice.default(for: .audio) + // verify works without audio permissions + let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice!) + if session.canAddInput(audioDeviceInput) { + // self.session.addInputWithNoConnections(audioDeviceInput) + session.addInput(audioDeviceInput) + self.audioDeviceInput = audioDeviceInput + } else { + owsFailDebug("Could not add audio device input to the session") + } + } + + func stopAudioCapture() { + assertIsOnSessionQueue() + + self.session.beginConfiguration() + defer { self.session.commitConfiguration() } + + guard let audioDeviceInput = self.audioDeviceInput else { + owsFailDebug("audioDevice was unexpectedly nil") + return + } + session.removeInput(audioDeviceInput) + self.audioDeviceInput = nil + audioSession.endAudioActivity(recordingAudioActivity) + } func startCapture() -> Promise { return sessionQueue.async(.promise) { [weak self] in @@ -48,15 +94,6 @@ class PhotoCapture: NSObject { try self.updateCurrentInput(position: .back) - let audioDevice = AVCaptureDevice.default(for: .audio) - // verify works without audio permissions - let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice!) - if self.session.canAddInput(audioDeviceInput) { - self.session.addInput(audioDeviceInput) - } else { - owsFailDebug("Could not add audio device input to the session") - } - guard let photoOutput = self.captureOutput.photoOutput else { throw PhotoCaptureError.initializationFailed } @@ -290,19 +327,21 @@ extension PhotoCapture: CaptureButtonDelegate { AssertIsOnMainThread() Logger.verbose("") - sessionQueue.async { + sessionQueue.async(.promise) { + try self.startAudioCapture() self.captureOutput.beginVideo(delegate: self) - - DispatchQueue.main.async { - self.delegate?.photoCaptureDidBeginVideo(self) - } - } + }.done { + self.delegate?.photoCaptureDidBeginVideo(self) + }.catch { error in + self.delegate?.photoCapture(self, processingDidError: error) + }.retainUntilComplete() } func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) { Logger.verbose("") sessionQueue.async { self.captureOutput.completeVideo(delegate: self) + self.stopAudioCapture() } AssertIsOnMainThread() // immediately inform UI that capture is stopping @@ -312,6 +351,9 @@ extension PhotoCapture: CaptureButtonDelegate { func didCancelLongPressCaptureButton(_ captureButton: CaptureButton) { Logger.verbose("") AssertIsOnMainThread() + sessionQueue.async { + self.stopAudioCapture() + } delegate?.photoCaptureDidCancelVideo(self) } @@ -369,8 +411,11 @@ extension PhotoCapture: CaptureOutputDelegate { AssertIsOnMainThread() if let error = error { - delegate?.photoCapture(self, processingDidError: error) - return + guard didSucceedDespiteError(error) else { + delegate?.photoCapture(self, processingDidError: error) + return + } + Logger.info("Ignoring error, since capture succeeded.") } let dataSource = DataSourcePath.dataSource(with: outputFileURL, shouldDeleteOnDeallocation: true) @@ -378,6 +423,19 @@ extension PhotoCapture: CaptureOutputDelegate { let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String) delegate?.photoCapture(self, didFinishProcessingAttachment: attachment) } + + /// The AVCaptureFileOutput can return an error even though recording succeeds. + /// I can't find useful documentation on this, but Apple's example AVCam app silently + /// discards these errors, so we do the same. + /// These spurious errors can be reproduced 1/3 of the time when making a series of short videos. + private func didSucceedDespiteError(_ error: Error) -> Bool { + let nsError = error as NSError + guard let successfullyFinished = nsError.userInfo[AVErrorRecordingSuccessfullyFinishedKey] as? Bool else { + return false + } + + return successfullyFinished + } } // MARK: - Capture Adapter @@ -410,6 +468,10 @@ class CaptureOutput { } movieOutput = AVCaptureMovieFileOutput() + // disable movie fragment writing since it's not supported on mp4 + // leaving it enabled causes all audio to be lost on videos longer + // than the default length (10s). + movieOutput.movieFragmentInterval = CMTime.invalid } var photoOutput: AVCaptureOutput? { diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index bd9452b7b..7cf5b32ba 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -45,24 +45,11 @@ class PhotoCaptureViewController: OWSViewController { } } - // MARK: - Dependencies - - var audioActivity: AudioActivity? - var audioSession: OWSAudioSession { - return Environment.shared.audioSession - } - // MARK: - Overrides override func loadView() { self.view = UIView() self.view.backgroundColor = Colors.navigationBarBackground - - let audioActivity = AudioActivity(audioDescription: "PhotoCaptureViewController", behavior: .playAndRecord) - self.audioActivity = audioActivity - if !self.audioSession.startAudioActivity(audioActivity) { - owsFailDebug("unexpectedly unable to start audio activity") - } } override func viewDidLoad() {