Przeglądaj źródła

reimplementing AudioRecorderController in swift

cyberta 5 lat temu
rodzic
commit
97a9668fa7

+ 16 - 0
deltachat-ios.xcodeproj/project.pbxproj

@@ -10,6 +10,8 @@
 		300C509D234B551900F8AE22 /* TextMediaMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300C509C234B551900F8AE22 /* TextMediaMessageCell.swift */; };
 		300C50A1234BDAB800F8AE22 /* TextMediaMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300C50A0234BDAB800F8AE22 /* TextMediaMessageSizeCalculator.swift */; };
 		30149D9322F21129003C12B5 /* QrViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30149D9222F21129003C12B5 /* QrViewController.swift */; };
+		3015634423A003BA00E9DEF4 /* AudioRecorderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3015634323A003BA00E9DEF4 /* AudioRecorderController.swift */; };
+		3015634723A0040200E9DEF4 /* AudioRecorderConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3015634623A0040200E9DEF4 /* AudioRecorderConstants.swift */; };
 		3022E6BE22E8768800763272 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3022E6C022E8768800763272 /* InfoPlist.strings */; };
 		30260CA7238F02F700D8D52C /* MultilineTextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30260CA6238F02F700D8D52C /* MultilineTextFieldCell.swift */; };
 		302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AC265E237F1807002A943F /* AvatarHelper.swift */; };
@@ -163,6 +165,8 @@
 		300C509C234B551900F8AE22 /* TextMediaMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextMediaMessageCell.swift; sourceTree = "<group>"; };
 		300C50A0234BDAB800F8AE22 /* TextMediaMessageSizeCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextMediaMessageSizeCalculator.swift; sourceTree = "<group>"; };
 		30149D9222F21129003C12B5 /* QrViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrViewController.swift; sourceTree = "<group>"; };
+		3015634323A003BA00E9DEF4 /* AudioRecorderController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderController.swift; sourceTree = "<group>"; };
+		3015634623A0040200E9DEF4 /* AudioRecorderConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderConstants.swift; sourceTree = "<group>"; };
 		3022E6BF22E8768800763272 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		3022E6C122E8768C00763272 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		3022E6C222E8768E00763272 /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sq; path = sq.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -395,6 +399,15 @@
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
+		3015634523A003D300E9DEF4 /* AudioRecorder */ = {
+			isa = PBXGroup;
+			children = (
+				3015634323A003BA00E9DEF4 /* AudioRecorderController.swift */,
+				3015634623A0040200E9DEF4 /* AudioRecorderConstants.swift */,
+			);
+			path = AudioRecorder;
+			sourceTree = "<group>";
+		};
 		3059617E234610A800C80F33 /* MessageKit */ = {
 			isa = PBXGroup;
 			children = (
@@ -657,6 +670,7 @@
 		AE851AC0227C693B00ED86F0 /* Controller */ = {
 			isa = PBXGroup;
 			children = (
+				3015634523A003D300E9DEF4 /* AudioRecorder */,
 				AE18F28B228C17630007B1BE /* AccountSetup */,
 				AEE56D752253431E007DC082 /* AccountSetupController.swift */,
 				AE0D26FC1FB1FE88002FAFCE /* ChatListController.swift */,
@@ -1033,6 +1047,7 @@
 				305961F02346125100C80F33 /* NSConstraintLayoutSet.swift in Sources */,
 				3059620E234614E700C80F33 /* DcContact+Extension.swift in Sources */,
 				305961F72346125100C80F33 /* MessageCollectionViewCell.swift in Sources */,
+				3015634723A0040200E9DEF4 /* AudioRecorderConstants.swift in Sources */,
 				AE851AC9227C77CF00ED86F0 /* Media.swift in Sources */,
 				AEACE2DF1FB3246400DCDD78 /* Message.swift in Sources */,
 				AE9DAF0F22C278C6004C9591 /* ChatTitleView.swift in Sources */,
@@ -1070,6 +1085,7 @@
 				305961E52346125100C80F33 /* LabelAlignment.swift in Sources */,
 				305961E82346125100C80F33 /* Sender.swift in Sources */,
 				305961EE2346125100C80F33 /* AvatarPosition.swift in Sources */,
+				3015634423A003BA00E9DEF4 /* AudioRecorderController.swift in Sources */,
 				AE25F09022807AD800CDEA66 /* AvatarSelectionCell.swift in Sources */,
 				302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */,
 				305961E62346125100C80F33 /* LocationMessageSnapshotOptions.swift in Sources */,

+ 23 - 0
deltachat-ios/Assets.xcassets/audio_record.imageset/Contents.json

@@ -0,0 +1,23 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "filename" : "audio_record.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "audio_record@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "audio_record@3x.png",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

BIN
deltachat-ios/Assets.xcassets/audio_record.imageset/audio_record.png


BIN
deltachat-ios/Assets.xcassets/audio_record.imageset/audio_record@2x.png


BIN
deltachat-ios/Assets.xcassets/audio_record.imageset/audio_record@3x.png


+ 6 - 0
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 23 - 0
deltachat-ios/Assets.xcassets/microphone_access.imageset/Contents.json

@@ -0,0 +1,23 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "filename" : "microphone_access.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "microphone_access@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "microphone_access@3x.png",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

BIN
deltachat-ios/Assets.xcassets/microphone_access.imageset/microphone_access.png


BIN
deltachat-ios/Assets.xcassets/microphone_access.imageset/microphone_access@2x.png


BIN
deltachat-ios/Assets.xcassets/microphone_access.imageset/microphone_access@3x.png


+ 3 - 0
deltachat-ios/Controller/AudioRecorder/AudioRecorderConstants.swift

@@ -0,0 +1,3 @@
+
+
+import Foundation

+ 296 - 0
deltachat-ios/Controller/AudioRecorder/AudioRecorderController.swift

@@ -0,0 +1,296 @@
+import Foundation
+import SCSiriWaveformView
+import AVKit
+
+protocol AudioRecorderControllerDelegate: class {
+    func didFinishAudioAtPath(path: String)
+}
+
+class AudioRecorderController: UIViewController, AVAudioRecorderDelegate {
+
+    weak var delegate: AudioRecorderControllerDelegate?
+
+    //Recording...
+    var meterUpdateDisplayLink: CADisplayLink?
+    var isRecordingPaused: Bool = false
+
+    // maximumRecordDuration > 0 -> restrict max time period for one take
+    var maximumRecordDuration = 0.0
+
+    //Private variables
+    var oldSessionCategory: AVAudioSession.Category?
+    var wasIdleTimerDisabled: Bool = false
+
+    var recordingFilePath: String = ""
+    var audioRecorder: AVAudioRecorder?
+
+    var normalTintColor: UIColor = UIColor.sendButtonBlue
+    var highlightedTintColor = UIColor.red
+
+    var isFirstUsage: Bool = true
+
+    lazy var musicFlowView: SCSiriWaveformView = {
+        let view = SCSiriWaveformView()
+        view.alpha = 1.0
+        view.backgroundColor = .clear
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.primaryWaveLineWidth = 3.0
+        view.secondaryWaveLineWidth = 1.0
+        return view
+    }()
+
+    //Access
+    var viewMicrophoneDenied: UIView = {
+        return UIView()
+    }()
+
+    var navigationTitle: NSString?
+
+    lazy var cancelButton: UIBarButtonItem = {
+        let button = UIBarButtonItem.init(barButtonSystemItem: UIBarButtonItem.SystemItem.cancel,
+                                          target: self,
+                                          action: #selector(cancelAction))
+        return button
+    }()
+
+    lazy var doneButton: UIBarButtonItem = {
+        let button = UIBarButtonItem.init(barButtonSystemItem: UIBarButtonItem.SystemItem.done,
+                                          target: self,
+                                          action: #selector(doneAction))
+        return button
+    }()
+
+    lazy var cancelRecordingButton: UIBarButtonItem = {
+        let button = UIBarButtonItem.init(barButtonSystemItem: UIBarButtonItem.SystemItem.cancel,
+                                                 target: self,
+                                                 action: #selector(cancelRecordingAction))
+        button.tintColor = highlightedTintColor
+        return button
+    }()
+
+    lazy var pauseButton: UIBarButtonItem = {
+        let button = UIBarButtonItem.init(barButtonSystemItem: UIBarButtonItem.SystemItem.pause,
+                                          target: self,
+                                          action: #selector(pauseRecordingButtonAction))
+        button.tintColor = highlightedTintColor
+        return button
+    }()
+
+    lazy var startRecordingButton: UIBarButtonItem = {
+        let button =  UIBarButtonItem.init(image: UIImage(named: "audio_record"),
+                                           style: UIBarButtonItem.Style.plain,
+                                           target: self,
+                                           action: #selector(recordingButtonAction))
+        button.tintColor = normalTintColor
+        return button
+    }()
+
+    lazy var continueRecordingButton: UIBarButtonItem = {
+        let button = UIBarButtonItem.init(image: UIImage(named: "audio_record"),
+                                          style: UIBarButtonItem.Style.plain,
+                                          target: self,
+                                          action: #selector(continueRecordingButtonAction))
+        button.tintColor = highlightedTintColor
+        return button
+    }()
+
+    lazy var flexItem = {
+        return UIBarButtonItem.init(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil)
+    }()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        self.view.backgroundColor = UIColor.themeColor(light: .white, dark: .black)
+        self.navigationController?.isToolbarHidden = false
+        self.navigationController?.toolbar.isTranslucent = true
+        self.navigationController?.navigationBar.isTranslucent = true
+
+        self.navigationItem.title = String.localized("voice_message")
+        self.navigationItem.leftBarButtonItem = cancelButton
+        self.navigationItem.rightBarButtonItem = doneButton
+        musicFlowView.frame = self.view.bounds
+        self.view.addSubview(musicFlowView)
+
+        self.view.addConstraints([
+            musicFlowView.constraintCenterXTo(view),
+            musicFlowView.constraintCenterYTo(view),
+            musicFlowView.constraintAlignTopTo(view),
+            musicFlowView.constraintAlignBottomTo(view),
+            musicFlowView.constraintAlignLeadingTo(view),
+            musicFlowView.constraintAlignTrailingTo(view)
+        ])
+
+        self.navigationController?.toolbar.tintColor = normalTintColor
+        self.navigationController?.navigationBar.tintColor = normalTintColor
+        self.navigationController?.navigationBar.isTranslucent = true
+        self.navigationController?.toolbar.isTranslucent = true
+
+        //Define the recorder setting
+        let recordSettings = [AVFormatIDKey: kAudioFormatMPEG4AAC,
+                              AVSampleRateKey: 44100.0,
+                              AVNumberOfChannelsKey: 1] as [String: Any]
+        let globallyUniqueString = ProcessInfo.processInfo.globallyUniqueString
+        recordingFilePath = NSTemporaryDirectory().appending(globallyUniqueString).appending(".m4a")
+        _ = try? audioRecorder = AVAudioRecorder.init(url: URL(fileURLWithPath: recordingFilePath), settings: recordSettings)
+        audioRecorder?.delegate = self
+        audioRecorder?.isMeteringEnabled = true
+    }
+
+
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+
+        startUpdatingMeter()
+
+        wasIdleTimerDisabled = UIApplication.shared.isIdleTimerDisabled
+        NotificationCenter.default.addObserver(self,
+                                               selector: #selector(didBecomeActiveNotification),
+                                               name: UIApplication.didBecomeActiveNotification,
+                                               object: nil)
+        validateMicrophoneAccess()
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+
+        NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
+        audioRecorder?.delegate = nil
+        audioRecorder?.stop()
+        audioRecorder = nil
+        stopUpdatingMeter()
+        UIApplication.shared.isIdleTimerDisabled = wasIdleTimerDisabled
+    }
+
+    func startUpdatingMeter() {
+        meterUpdateDisplayLink?.invalidate()
+        meterUpdateDisplayLink = CADisplayLink.init(target: self, selector: #selector(updateMeters))
+        meterUpdateDisplayLink?.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
+    }
+
+    func stopUpdatingMeter() {
+        meterUpdateDisplayLink?.invalidate()
+        meterUpdateDisplayLink = nil
+    }
+
+    @objc func updateMeters() {
+        if let audioRecorder = audioRecorder {
+            if audioRecorder.isRecording || isRecordingPaused {
+                audioRecorder.updateMeters()
+                let normalizedValue: Float = pow(10, audioRecorder.averagePower(forChannel: 0) / 20)
+                musicFlowView.waveColor = highlightedTintColor
+                musicFlowView.update(withLevel: CGFloat(normalizedValue))
+                self.navigationItem.title = String.timeStringForInterval(audioRecorder.currentTime)
+            } else {
+                musicFlowView.waveColor = normalTintColor
+                musicFlowView.update(withLevel: 0)
+            }
+        }
+    }
+
+    @objc func recordingButtonAction() {
+        logger.debug("start recording")
+        self.setToolbarItems([flexItem, pauseButton, flexItem], animated: true)
+        self.navigationItem.setLeftBarButton(cancelRecordingButton, animated: true)
+        if FileManager.default.fileExists(atPath: recordingFilePath) {
+            _ = try? FileManager.default.removeItem(atPath: recordingFilePath)
+        }
+        let session = AVAudioSession.sharedInstance()
+        oldSessionCategory = session.category
+        _ = try? session.setCategory(AVAudioSession.Category.record)
+        UIApplication.shared.isIdleTimerDisabled = true
+        audioRecorder?.prepareToRecord()
+        isRecordingPaused = false
+
+        if maximumRecordDuration <= 0 {
+            audioRecorder?.record()
+        } else {
+            audioRecorder?.record(forDuration: maximumRecordDuration)
+        }
+    }
+
+    @objc func continueRecordingButtonAction() {
+        logger.debug("continue recording")
+        self.setToolbarItems([flexItem, pauseButton, flexItem], animated: true)
+        isRecordingPaused = false
+        audioRecorder?.record()
+    }
+
+    @objc func pauseRecordingButtonAction() {
+        logger.debug("pause")
+        isRecordingPaused = true
+        audioRecorder?.pause()
+        self.setToolbarItems([flexItem, continueRecordingButton, flexItem], animated: true)
+    }
+
+    @objc func cancelRecordingAction() {
+        logger.debug("cancel recording")
+        isRecordingPaused = false
+        audioRecorder?.stop()
+        _ = try? FileManager.default.removeItem(atPath: recordingFilePath)
+        self.navigationItem.title = String.localized("voice_message")
+    }
+
+    @objc func cancelAction() {
+        logger.debug("cancel Action")
+        dismiss(animated: true, completion: nil)
+    }
+
+    @objc func doneAction() {
+        logger.debug("done with Action")
+        isRecordingPaused = false
+        audioRecorder?.stop()
+        if let delegate = self.delegate {
+            delegate.didFinishAudioAtPath(path: recordingFilePath)
+        }
+        dismiss(animated: true, completion: nil)
+    }
+
+    @objc func didBecomeActiveNotification() {
+        validateMicrophoneAccess()
+    }
+
+    func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
+        if flag {
+            self.setToolbarItems([flexItem, startRecordingButton, flexItem], animated: true)
+            if let oldSessionCategory = oldSessionCategory {
+               _ = try? AVAudioSession.sharedInstance().setCategory(oldSessionCategory)
+               UIApplication.shared.isIdleTimerDisabled = wasIdleTimerDisabled
+            }
+        } else {
+            try? FileManager.default.removeItem(at: URL(fileURLWithPath: recordingFilePath))
+        }
+         self.navigationItem.setLeftBarButton(cancelButton, animated: true)
+    }
+
+    func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) {
+        logger.error("audio recording failed: \(error?.localizedDescription ?? "unknown")")
+    }
+
+    func validateMicrophoneAccess() {
+        let audioSession = AVAudioSession.sharedInstance()
+        audioSession.requestRecordPermission({(granted: Bool) -> Void in
+            if granted {
+
+                DispatchQueue.main.async { [weak self] in
+                    //self?.viewMicrophoneDenied.alpha =
+                    //self?.musicFlowView.alpha = granted ? 1.0 : 0.0
+                    if let self = self {
+                        if self.isFirstUsage {
+                            if !granted {
+                                self.setToolbarItems([self.flexItem, self.startRecordingButton, self.flexItem], animated: true)
+                                self.startRecordingButton.isEnabled = false
+                            } else {
+                                self.pauseButton.isEnabled = granted
+                                self.recordingButtonAction()
+                            }
+
+                            self.isFirstUsage = false
+                        } else {
+                            self.startRecordingButton.isEnabled = granted
+                        }
+                    }
+                }
+            }
+        })
+    }
+}

+ 13 - 0
deltachat-ios/Extensions/String+Extension.swift

@@ -73,4 +73,17 @@ extension String {
         let resultString: String = String.localizedStringWithFormat(formatString, count)
         return resultString
     }
+
+    static func timeStringForInterval(_ interval: TimeInterval) -> String {
+        let time = NSInteger(interval)
+        let seconds = time % 60
+        let minutes = (time / 60) % 60
+        let hours = time / 3600
+
+        if hours > 0 {
+            return NSString.localizedStringWithFormat("%02li:%02li:%02li", hours, minutes, seconds) as String
+        } else {
+            return NSString.localizedStringWithFormat("%02li:%02li", minutes, seconds) as String
+        }
+    }
 }

+ 9 - 18
deltachat-ios/Helper/MediaPicker.swift

@@ -23,7 +23,7 @@ extension MediaPickerDelegate {
     }
 }
 
-class MediaPicker: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate, IQAudioRecorderViewControllerDelegate {
+class MediaPicker: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate, AudioRecorderControllerDelegate {
 
     private let navigationController: UINavigationController
     private weak var delegate: MediaPickerDelegate?
@@ -35,17 +35,13 @@ class MediaPicker: NSObject, UINavigationControllerDelegate, UIImagePickerContro
 
     func showVoiceRecorder(delegate: MediaPickerDelegate) {
         self.delegate = delegate
-        let audioRecorderController = IQAudioRecorderViewController()
+        let audioRecorderController = AudioRecorderController()
         audioRecorderController.delegate = self
-        audioRecorderController.title = String.localized("voice_message")
-        audioRecorderController.allowCropping = false
-        audioRecorderController.allowPlayback = false
-        audioRecorderController.recordOnCreation = true
-
-        audioRecorderController.maximumRecordDuration = 1200
-        audioRecorderController.barStyle = .default
-        navigationController.presentAudioRecorderViewControllerAnimated(audioRecorderController)
-    }
+        //audioRecorderController.maximumRecordDuration = 1200
+        let audioRecorderNavController = UINavigationController(rootViewController: audioRecorderController)
+
+        navigationController.present(audioRecorderNavController, animated: true, completion: nil)
+ }
 
     func showPhotoVideoLibrary(delegate: MediaPickerDelegate) {
         if PHPhotoLibrary.authorizationStatus() != .authorized {
@@ -138,14 +134,9 @@ class MediaPicker: NSObject, UINavigationControllerDelegate, UIImagePickerContro
         navigationController.dismiss(animated: true, completion: nil)
     }
 
-    func audioRecorderController(_ controller: IQAudioRecorderViewController, didFinishWithAudioAtPath filePath: String) {
-        let url = NSURL(fileURLWithPath: filePath)
+    func didFinishAudioAtPath(path: String) {
+        let url = NSURL(fileURLWithPath: path)
         self.delegate?.onVoiceMessageRecorded(url: url)
-        controller.dismiss(animated: true, completion: nil)
-    }
-
-    func audioRecorderControllerDidCancel(_ controller: IQAudioRecorderViewController) {
-        controller.dismiss(animated: true, completion: nil)
     }
 
 }