美文网首页
iOS Swift H5 WKWebView交互麦克风录音完访问

iOS Swift H5 WKWebView交互麦克风录音完访问

作者: Lee坚武 | 来源:发表于2021-12-30 15:29 被阅读0次
    ##更多方法交流可以家魏鑫:lixiaowu1129,一起探讨iOS相关技术!
    

    需求分析:

    最近项目需求需要麦克风录音权限,因为整体上的UI界面是前端wkwebview搭建的,实现功能逻辑是由iOS实现,没有用原生!然后就出现了需要麦克风录音机跟H5交互的功能模块!

    查了资料都文章说iOS对h5交互麦克风录音不友好
    现在具体工作流程步骤如下:

    1. 首先创建了一个wkwebview
    //加载webview视图
        override func loadView() {
            let preference = WKPreferences()
            preference.minimumFontSize = 0
            preference.javaScriptEnabled = true
            preference.javaScriptCanOpenWindowsAutomatically = true
            preference.setValue("TRUE", forKey: "allowFileAccessFromFileURLs")
            debugPrint("这里已经进来了")
            
            // swift 提供给 h5 调用方法
            let userContentController = WKUserContentController()
            userContentController.add(self, name: "callAudio")  //调起iOS音频权限
            userContentController.add(self, name: "recorderStart")  //开始录音
            userContentController.add(self, name: "recorderStop")  //停止录音
            
            let conf = WKWebViewConfiguration()
            conf.userContentController = userContentController
            conf.preferences = preference
            
    //        let conf = WKWebViewConfiguration();
    //        conf.userContentController.add(self, name: "callAudio")  //调起iOS音频权限
    //        conf.userContentController.add(self, name: "recorderStart")  //开始录音
    //        conf.userContentController.add(self, name: "recorderStop")  //停止录音
            webView = WKWebView(frame: CGRect(x:0, y:0, width:SCREEN_WIDTH, height:SCREEN_HEIGHT), configuration: conf)
            webView.navigationDelegate = self;
            webView.scrollView.isScrollEnabled = false  //禁止webview滑动滚动
            if #available(iOS 11.0, *) {
                webView.scrollView.contentInsetAdjustmentBehavior = .never;
            }
            view = webView;
        }
    

    其中:callAudio、recorderStart、recorderStop是iOS跟webview定义好协议接收的方法

    1. 重点:加载完成后接收H5调用的协议方法:
    // 接受 h5 调用
        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
            guard let name = message.value(forKey: "name") as? String, let body = message.value(forKey: "body") as? String  else { return }
            debugPrint("测试链接8888+:\(name)")
            if name == "callAudio" {
                SystemAuth.authMicrophone { result in
                    if result{
                        self.webView.evaluateJavaScript("getPermission('\(result)')", completionHandler: nil)
                    }else{
                        DispatchQueue.main.async {
                            let alertView = UIAlertView(title: "无法访问您的麦克风" , message: "请到设置 -> 隐私 -> 麦克风 ,打开访问权限", delegate: nil, cancelButtonTitle: "取消", otherButtonTitles: "好的")
                            alertView.show()
                        }
                    }
                }
            }
    }
    

    SystemAuth.authMicrophone 调用录音麦克风权限返回true跟false
    self.webView.evaluateJavaScript("getPermission('(result)')", completionHandler: nil) iOS拦截到方法注入新方法getPermission()携带参数true或者false返回给H5接收

    Swift开启iOS的录音权限包括其他照相机权限的代码文件

    我整理好在下面的代码了

    SystemAuth.Swift

    //
    //  SystemAuth.swift
    //  Authorization
    //
    //  Created by 柯南 on 2020/9/4.
    //  Copyright © 2020 LTM. All rights reserved.
    //
    
    import UIKit
    
    /// 媒体资料库/Apple Music
    import MediaPlayer
    import Photos
    import UserNotifications
    import Contacts
    /// Siri权限
    import Intents
    /// 语音转文字权限
    import Speech
    /// 日历、提醒事项
    import EventKit
    /// Face、TouchID
    import LocalAuthentication
    import HealthKit
    import HomeKit
    /// 运动与健身权限
    import CoreMotion
    /// 防止获取无效 计步器
    private let cmPedometer = CMPedometer()
    
    typealias AuthClouser = ((Bool)->())
    
    /// 定义私有全局变量,解决在iOS 13 定位权限弹框自动消失的问题
    private let locationAuthManager = CLLocationManager()
    
    /**
     escaping 逃逸闭包的生命周期:
     
     1,闭包作为参数传递给函数;
     
     2,退出函数;
     
     3,闭包被调用,闭包生命周期结束
     即逃逸闭包的生命周期长于函数,函数退出的时候,逃逸闭包的引用仍被其他对象持有,不会在函数结束时释放
     经常使用逃逸闭包的2个场景:
     异步调用: 如果需要调度队列中异步调用闭包,比如网络请求成功的回调和失败的回调,这个队列会持有闭包的引用,至于什么时候调用闭包,或闭包什么时候运行结束都是不确定,上边的例子。
     存储: 需要存储闭包作为属性,全局变量或其他类型做稍后使用,例子待补充
     */
    public class SystemAuth {
        
    //    /**
    //     媒体资料库/Apple Music权限
    //
    //     - parameters: action 权限结果闭包
    //     */
    //    class func authMediaPlayerService(clouser :@escaping AuthClouser) {
    //        let authStatus = MPMediaLibrary.authorizationStatus()
    //        switch authStatus {
    //        /// 未作出选择
    //        case .notDetermined:
    //            MPMediaLibrary.requestAuthorization { (status) in
    //                if status == .authorized{
    //                    DispatchQueue.main.async {
    //                        clouser(true)
    //                    }
    //                }else{
    //                    DispatchQueue.main.async {
    //                        clouser(false)
    //                    }
    //                }
    //            }
    //        /// 用户明确拒绝此应用程序的授权,或在设置中禁用该服务。
    //        case .denied:
    //            clouser(false)
    //        /// 该应用程序未被授权使用该服务。由于用户无法改变对该服务的主动限制。此状态,并且个人可能没有拒绝授权。
    //        case .restricted:
    //            clouser(false)
    //        /// 已授权
    //        case .authorized:
    //            clouser(true)
    //        /// 扩展以后可能有的状态,做保护措施
    //        @unknown default:
    //            clouser(false)
    //        }
    //    }
        
    //    /**
    //     联网权限
    //
    //     - parameters: action 权限结果闭包
    //     */
    //    class func authNetwork(clouser: @escaping AuthClouser) {
    //
    //        let reachabilityManager = NetworkReachabilityManager(host: "www.baidu.com")
    //        switch reachabilityManager?.status {
    //        case .reachable(.cellular):
    //            clouser(true)
    //        case .reachable(.ethernetOrWiFi):
    //            clouser(true)
    //        case .none:
    //            clouser(false)
    //        case .notReachable:
    //            clouser(false)
    //            //            let status = reachabilityManager?.flags
    //            //            switch status {
    //            //            case .none:
    //            //                clouser(false)
    //            //            case .some(.connectionAutomatic):
    //            //                clouser(false)
    //            //            case .some(.connectionOnDemand):
    //            //                clouser(false)
    //            //            case .some(.connectionOnTraffic):
    //            //                clouser(false)
    //            //            case .some(.connectionRequired):
    //            //                clouser(false)
    //            //            case .some(.interventionRequired):
    //            //                clouser(false)
    //            //            case .some(.isDirect):
    //            //                clouser(false)
    //            //            case .some(.isLocalAddress):
    //            //                clouser(false)
    //            //            case .some(.isWWAN):
    //            //                clouser(false)
    //            //            case .some(.reachable):
    //            //                clouser(false)
    //            //            case .some(.transientConnection):
    //            //                clouser(false)
    //            //            case .init(rawValue: 0):
    //            //                clouser(false)
    //            //            case .some(_):
    //            //                clouser(false)
    //        //            }
    //        case .unknown:
    //            clouser(false)
    //        }
    //    }
        
        /**
         相机权限
         
         - parameters: action 权限结果闭包
         */
        class func authCamera(clouser: @escaping AuthClouser) {
            let authStatus = AVCaptureDevice.authorizationStatus(for: .video)
            switch authStatus {
            case .notDetermined:
                AVCaptureDevice.requestAccess(for: .video) { (result) in
                    if result{
                        DispatchQueue.main.async {
                            clouser(true)
                        }
                    }else{
                        DispatchQueue.main.async {
                            clouser(false)
                        }
                    }
                }
            case .denied:
                clouser(false)
            case .restricted:
                clouser(false)
            case .authorized:
                clouser(true)
            @unknown default:
                clouser(false)
            }
        }
        
        /**
         相册权限
         
         - parameters: action 权限结果闭包
         */
        class func authPhotoLib(clouser: @escaping AuthClouser) {
            let authStatus = PHPhotoLibrary.authorizationStatus()
            switch authStatus {
            case .notDetermined:
                PHPhotoLibrary.requestAuthorization { (status) in
                    if status == .authorized{
                        DispatchQueue.main.async {
                            clouser(true)
                        }
                    }else{
                        DispatchQueue.main.async {
                            clouser(false)
                        }
                    }
                }
            case .denied:
                clouser(false)
            case .restricted:
                clouser(false)
            case .authorized:
                clouser(true)
            @unknown default:
                clouser(false)
            }
        }
        
        /**
         麦克风权限
         
         - parameters: action 权限结果闭包
         */
        class func authMicrophone(clouser: @escaping AuthClouser) {
            let authStatus = AVAudioSession.sharedInstance().recordPermission
            switch authStatus {
            case .undetermined:
                AVAudioSession.sharedInstance().requestRecordPermission { (result) in
                    if result{
                        DispatchQueue.main.async {
                            clouser(true)
                        }
                    }else{
                        DispatchQueue.main.async {
                            clouser(false)
                        }
                    }
                }
            case .denied:
                clouser(false)
            case .granted:
                clouser(true)
            @unknown default:
                clouser(false)
            }
        }
        
        //开启麦克风权限
        func openAudioSession() {
            let permissionStatus = AVAudioSession.sharedInstance().recordPermission
            if permissionStatus == AVAudioSession.RecordPermission.undetermined {
                AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
                    //此处可以判断权限状态来做出相应的操作,如改变按钮状态
                    if granted{
                        DispatchQueue.main.async {
    
                        }
                    }else{
                        DispatchQueue.main.async {
                            let alertView = UIAlertView(title: "无法访问您的麦克风" , message: "请到设置 -> 隐私 -> 麦克风 ,打开访问权限", delegate: nil, cancelButtonTitle: "取消", otherButtonTitles: "好的")
                                            alertView.show()
                        }
                    }
                }
            }
        }
        
        //是否开启麦克风
        func getPermission() -> Bool{
            let authStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType.audio)
            return authStatus != .restricted && authStatus != .denied
        }
        
        /**
         定位权限
         
         - parameters: action 权限结果闭包(有无权限,是否第一次请求权限)
         */
        class func authLocation(clouser: @escaping ((Bool,Bool)->())) {
            let authStatus = CLLocationManager.authorizationStatus()
            switch authStatus {
            case .notDetermined:
                //由于IOS8中定位的授权机制改变 需要进行手动授权
                locationAuthManager.requestAlwaysAuthorization()
                locationAuthManager.requestWhenInUseAuthorization()
                let status = CLLocationManager.authorizationStatus()
                if  status == .authorizedAlways || status == .authorizedWhenInUse {
                    DispatchQueue.main.async {
                        clouser(true && CLLocationManager.locationServicesEnabled(), true)
                    }
                }else{
                    DispatchQueue.main.async {
                        clouser(false, true)
                    }
                }
            case .restricted:
                clouser(false, false)
            case .denied:
                clouser(false, false)
            case .authorizedAlways:
                clouser(true && CLLocationManager.locationServicesEnabled(), false)
            case .authorizedWhenInUse:
                clouser(true && CLLocationManager.locationServicesEnabled(), false)
            @unknown default:
                clouser(false, false)
            }
        }
        
    //    /**
    //     推送权限
    //
    //     - parameters: action 权限结果闭包
    //     */
    //    class func authNotification(clouser: @escaping AuthClouser){
    //        UNUserNotificationCenter.current().getNotificationSettings(){ (setttings) in
    //            switch setttings.authorizationStatus {
    //            case .notDetermined:
    //                UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .carPlay, .sound]) { (result, error) in
    //                    if result{
    //                        DispatchQueue.main.async {
    //                            clouser(true)
    //                        }
    //                    }else{
    //                        DispatchQueue.main.async {
    //                            clouser(false)
    //                        }
    //                    }
    //                }
    //            case .denied:
    //                clouser(false)
    //            case .authorized:
    //                clouser(true)
    //            case .provisional:
    //                clouser(true)
    //            @unknown default:
    //                clouser(false)
    //            }
    //        }
    //    }
        
        /**
         运动与健身
         
         - parameters: action 权限结果闭包
         */
        class func authCMPedometer(clouser: @escaping AuthClouser){
            cmPedometer.queryPedometerData(from: Date(), to: Date()) { (pedometerData, error) in
                if pedometerData?.numberOfSteps != nil{
                    DispatchQueue.main.async {
                        clouser(true)
                    }
                }else{
                    DispatchQueue.main.async {
                        clouser(false)
                    }
                }
            }
        }
        
        /**
         通讯录权限
         
         - parameters: action 权限结果闭包
         */
        class func authContacts(clouser: @escaping AuthClouser){
            let authStatus = CNContactStore.authorizationStatus(for: .contacts)
            switch authStatus {
            case .notDetermined:
                CNContactStore().requestAccess(for: .contacts) { (result, error) in
                    if result{
                        DispatchQueue.main.async {
                            clouser(true)
                        }
                    }else{
                        DispatchQueue.main.async {
                            clouser(false)
                        }
                    }
                }
            case .restricted:
                clouser(false)
            case .denied:
                clouser(false)
            case .authorized:
                clouser(true)
            @unknown default:
                clouser(false)
            }
        }
        
    //    /**
    //     Siri 权限
    //     
    //     - parameters: action 权限结果闭包
    //     */
    //    class func authSiri(clouser: @escaping AuthClouser){
    //        let authStatus = INPreferences.siriAuthorizationStatus()
    //        switch authStatus {
    //        case .notDetermined:
    //            INPreferences.requestSiriAuthorization { (status) in
    //                if status == .authorized{
    //                    DispatchQueue.main.async {
    //                        clouser(true)
    //                    }
    //                }else{
    //                    DispatchQueue.main.async {
    //                        clouser(false)
    //                    }
    //                }
    //            }
    //        case .restricted:
    //            clouser(false)
    //        case .denied:
    //            clouser(false)
    //        case .authorized:
    //            clouser(true)
    //        @unknown default:
    //            clouser(false)
    //        }
    //    }
        
        /**
         语音转文字权限
         
         - parameters: action 权限结果闭包
         */
        class func authSpeechRecognition(clouser: @escaping AuthClouser){
            if #available(iOS 10.0, *) {
                let authStatus = SFSpeechRecognizer.authorizationStatus()
                switch authStatus {
                case .notDetermined:
                    SFSpeechRecognizer.requestAuthorization { (status) in
                        if status == .authorized{
                            DispatchQueue.main.async {
                                clouser(true)
                            }
                        }else{
                            DispatchQueue.main.async {
                                clouser(false)
                            }
                        }
                    }
                case .restricted:
                    clouser(false)
                case .denied:
                    clouser(false)
                case .authorized:
                    clouser(true)
                @unknown default:
                    clouser(false)
                }
            } else {
                // Fallback on earlier versions
            }
        }
        
        /**
         提醒事项
         
         - parameters: action 权限结果闭包
         */
        class func authRreminder(clouser: @escaping AuthClouser){
            let authStatus = EKEventStore.authorizationStatus(for: .reminder)
            switch authStatus {
            case .notDetermined:
                EKEventStore().requestAccess(to: .reminder) { (result, error) in
                    if result{
                        DispatchQueue.main.async {
                            clouser(true)
                        }
                    }else{
                        DispatchQueue.main.async {
                            clouser(false)
                        }
                    }
                }
            case .restricted:
                clouser(false)
            case .denied:
                clouser(false)
            case .authorized:
                clouser(true)
            @unknown default:
                clouser(false)
            }
        }
        
        /**
         日历
         
         - parameters: action 权限结果闭包
         */
        class func authEvent(clouser: @escaping AuthClouser){
            let authStatus = EKEventStore.authorizationStatus(for: .event)
            switch authStatus {
            case .notDetermined:
                EKEventStore().requestAccess(to: .event) { (result, error) in
                    if result{
                        DispatchQueue.main.async {
                            clouser(true)
                        }
                    }else{
                        DispatchQueue.main.async {
                            clouser(false)
                        }
                    }
                }
            case .restricted:
                clouser(false)
            case .denied:
                clouser(false)
            case .authorized:
                clouser(true)
            @unknown default:
                clouser(false)
            }
        }
        
        /**
         FaceID或者TouchID 认证
         
         - parameters: action 权限结果闭包
         */
        class func authFaceOrTouchID(clouser: @escaping ((Bool,Error)->())) {
            let context = LAContext()
            var error: NSError?
            let result = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error)
            if result {
                context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "认证") { (success, authError) in
                    if success{
                        print("成功")
                    }else{
                        print("失败")
                    }
                }
            }else{
                /**
                 #define kLAErrorAuthenticationFailed                       -1
                 #define kLAErrorUserCancel                                 -2
                 #define kLAErrorUserFallback                               -3
                 #define kLAErrorSystemCancel                               -4
                 #define kLAErrorPasscodeNotSet                             -5
                 #define kLAErrorTouchIDNotAvailable                        -6
                 #define kLAErrorTouchIDNotEnrolled                         -7
                 #define kLAErrorTouchIDLockout                             -8
                 #define kLAErrorAppCancel                                  -9
                 #define kLAErrorInvalidContext                            -10
                 #define kLAErrorNotInteractive                          -1004
                 
                 #define kLAErrorBiometryNotAvailable              kLAErrorTouchIDNotAvailable
                 #define kLAErrorBiometryNotEnrolled               kLAErrorTouchIDNotEnrolled
                 
                 */
                print("不可以使用")
            }
        }
        
        /**
         健康  (写:体能训练、iOS13 听力图 读: 健身记录、体能训练、iOS13 听力图)
         
         - parameters: action 权限结果闭包
         */
        class func authHealth(clouser: @escaping AuthClouser){
            if HKHealthStore.isHealthDataAvailable(){
                let authStatus = HKHealthStore().authorizationStatus(for: .workoutType())
                switch authStatus {
                case .notDetermined:
                    if #available(iOS 13.0, *) {
                        HKHealthStore().requestAuthorization(toShare: [.audiogramSampleType(), .workoutType()], read: [.activitySummaryType(), .workoutType(), .audiogramSampleType()]) { (result, error) in
                            if result{
                                DispatchQueue.main.async {
                                    clouser(true)
                                }
                            }else{
                                DispatchQueue.main.async {
                                    clouser(false)
                                }
                            }
                        }
                    } else {
                        if #available(iOS 9.3, *) {
                            HKHealthStore().requestAuthorization(toShare: [.workoutType()], read: [.activitySummaryType(), .workoutType()]) { (result, error) in
                                if result{
                                    DispatchQueue.main.async {
                                        clouser(true)
                                    }
                                }else{
                                    DispatchQueue.main.async {
                                        clouser(false)
                                    }
                                }
                            }
                        } else {
                            // Fallback on earlier versions
                        }
                    }
                case .sharingDenied:
                    clouser(false)
                case .sharingAuthorized:
                    clouser(true)
                @unknown default:
                    clouser(false)
                }
            }else{
                clouser(false)
            }
        }
        
        /**
         家庭、住宅数据
         
         - parameters: action 权限结果闭包
         */
        class func authHomeKit(clouser: @escaping AuthClouser) {
            if #available(iOS 13.0, *) {
                switch HMHomeManager().authorizationStatus {
                case .authorized:
                    clouser(true)
                case .determined:
                    clouser(false)
                case .restricted:
                    clouser(false)
                default:
                    clouser(false)
                }
            } else {
                if (HMHomeManager().primaryHome != nil) {
                    clouser(true)
                }else{
                    clouser(false)
                }
            }
        }
        
        /**
         系统设置
         
         - parameters: urlString 可以为系统,也可以为微信:weixin://、QQ:mqq://
         - parameters: action 结果闭包
         */
        class func authSystemSetting(urlString :String?, clouser: @escaping AuthClouser) {
            var url: URL
            if (urlString != nil) && urlString?.count ?? 0 > 0 {
                url = URL(string: urlString!)!
            }else{
                url = URL(string: UIApplication.openSettingsURLString)!
            }
            
            if UIApplication.shared.canOpenURL(url){
                if #available(iOS 10.0, *) {
                    UIApplication.shared.open(url, options: [:]) { (result) in
                        if result{
                            clouser(true)
                        }else{
                            clouser(false)
                        }
                    }
                } else {
                    // Fallback on earlier versions
                }
            }else{
                clouser(false)
            }
        }
    }
    
    
    1. 重点来了 录制完音频文件后,也是跟前端定义好方法返回回去
      让前端利用base64编码发送回去给前端,剩下他就能接收处理
    let fileData = try! Data(contentsOf: wavFileURL)
          //将图片转为base64编码
    let base64 = fileData.base64EncodedString(options: .endLineWithLineFeed).addingPercentEncoding(withAllowedCharacters: .alphanumerics)
    UserDefaults.standard.setValue(base64, forKey: "wavFileURL")
    

    4.到这里,你就以为很成功了,能顺利录音-播放-展示到webview了吗?那你就继续入坑吧
    原本以为我这里通过base64把录完音的文件发送给前端就没我活了,谁知道录完音老是播放不了
    解决方法:

    1. 由于WKWebView无权限访问本地文件,访问本地文件使用的是file://协议,由于WKWebView的安全机制,会报一些错无法访问到。需要打开webView的file://协议访问权限,设置allowFileAccessFromFileURLs为true。

    2.如果出现跨域的报错,也可以通过设置allowUniversalAccessFromFileURLs为true来解决。

    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    // 解决HTML请求跨域
    [config setValue:@(true) forKey:@"allowUniversalAccessFromFileURLs"];
    
    WKPreferences *preferences = [[WKPreferences alloc] init];
    // 打开web访问本地文件权限
    [preferences setValue:@(true) forKey:@"allowFileAccessFromFileURLs"];
    config.preferences = preferences;
    
    
    let preference = WKPreferences()
            preference.minimumFontSize = 0
            preference.javaScriptEnabled = true
            preference.javaScriptCanOpenWindowsAutomatically = true
            preference.setValue("TRUE", forKey: "allowFileAccessFromFileURLs")
            debugPrint("这里已经进来了")
    
    image.png

    更多方法交流可以家魏鑫:lixiaowu1129,一起探讨iOS相关技术!

    相关文章

      网友评论

          本文标题:iOS Swift H5 WKWebView交互麦克风录音完访问

          本文链接:https://www.haomeiwen.com/subject/knbiqrtx.html