Repository: yxwandroid/flutter_plugin_record Branch: master Commit: 8e6f3cf9ebad Files: 95 Total size: 241.5 KB Directory structure: gitextract_l9gpf37n/ ├── .gitattributes ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── settings.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── kotlin/ │ └── record/ │ └── wilson/ │ └── flutter/ │ └── com/ │ └── flutter_plugin_record/ │ ├── FlutterPluginRecordPlugin.kt │ ├── timer/ │ │ ├── ITimer.java │ │ ├── ITimerChangeCallback.java │ │ ├── MTimer.java │ │ └── TimerUtils.java │ └── utils/ │ ├── AudioHandler.java │ ├── DateUtils.java │ ├── DialogUtil.java │ ├── FileTool.java │ ├── LogUtils.java │ ├── PlayState.java │ ├── PlayUtilsPlus.java │ └── RecorderUtil.java ├── example/ │ ├── .flutter-plugins-dependencies │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── android/ │ │ ├── .gitignore │ │ ├── app/ │ │ │ ├── build.gradle │ │ │ └── src/ │ │ │ ├── debug/ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── main/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── kotlin/ │ │ │ │ │ └── record/ │ │ │ │ │ └── wilson/ │ │ │ │ │ └── flutter/ │ │ │ │ │ └── com/ │ │ │ │ │ └── flutter_plugin_record_example/ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values/ │ │ │ │ │ └── styles.xml │ │ │ │ └── xml/ │ │ │ │ └── network_security_config.xml │ │ │ └── profile/ │ │ │ └── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ ├── res/ │ │ │ └── values/ │ │ │ └── strings_en.arb │ │ └── settings.gradle │ ├── ios/ │ │ ├── Flutter/ │ │ │ ├── .last_build_id │ │ │ ├── AppFrameworkInfo.plist │ │ │ ├── Debug.xcconfig │ │ │ ├── Flutter.podspec │ │ │ └── Release.xcconfig │ │ ├── Podfile │ │ ├── Runner/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── LaunchImage.imageset/ │ │ │ │ ├── Contents.json │ │ │ │ └── README.md │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ └── Runner-Bridging-Header.h │ │ ├── Runner.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata/ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── Runner.xcscheme │ │ └── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ ├── lib/ │ │ ├── generated/ │ │ │ └── i18n.dart │ │ ├── main.dart │ │ ├── path_provider_screen.dart │ │ ├── record_mp3_screen.dart │ │ ├── record_screen.dart │ │ └── wechat_record_screen.dart │ ├── pubspec.yaml │ └── test/ │ └── widget_test.dart ├── flutter_plugin_record.iml ├── ios/ │ ├── .gitignore │ ├── Assets/ │ │ └── .gitkeep │ ├── Classes/ │ │ ├── DPAudioPlayer.h │ │ ├── DPAudioPlayer.m │ │ ├── DPAudioRecorder.h │ │ ├── DPAudioRecorder.m │ │ ├── FlutterPluginRecordPlugin.h │ │ ├── FlutterPluginRecordPlugin.m │ │ ├── JX_GCDTimerManager.h │ │ └── JX_GCDTimerManager.m │ └── flutter_plugin_record.podspec ├── lib/ │ ├── const/ │ │ ├── play_state.dart │ │ ├── record_state.dart │ │ └── response.dart │ ├── flutter_plugin_record.dart │ ├── index.dart │ ├── utils/ │ │ └── common_toast.dart │ └── widgets/ │ ├── custom_overlay.dart │ └── voice_widget.dart ├── pubspec.yaml └── test/ └── flutter_plugin_record_test.dart ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.yml linguist-language=Dart *.java linguist-language=Dart *.m linguist-language=Dart *.h linguist-language=Dart ================================================ FILE: .gitignore ================================================ .DS_Store .dart_tool/ .packages .pub/ build/ .idea/ ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: 68587a0916366e9512a78df22c44163d041dd5f3 channel: stable project_type: plugin ================================================ FILE: CHANGELOG.md ================================================ ## 1.0.1 * 录音时长类型修改为 Double(和 iOS 端保持一致) ## 1.0.0 * 添加空安全相关逻辑 * 适配 flutter 2.0 ## 0.3.5 * 修复ios 音频监听灵敏性问题 ## 0.3.4 * 修复根据路径录制bug ## 0.3.3 * 修复根据路径录制bug ## 0.3.2 * 修复android 获取权限bug ## 0.3.1 * 更新IOS podfile ## 0.3.0 * 添加支持录制mp3 ## 0.2.5 * 修复权限申请bug ## 0.2.4 * 修复播放音频文件的bug。 ## 0.2.3 * 修复bug ## 0.2.2 * 修复bug ## 0.2.1 * 补充说明文档 ## 0.2.0 * 实现暂停播放和继续播放功能 ## 0.1.9 * 注释掉初始化插件就申请权限的问题 ## 0.1.8 * 添加支持 Android 和IOS 播放在线wav音频 ## 0.1.7 * 修复bug在未使用录音功能前,通过playByPath播发音频,音频可以正常播放,但无法监听到播放结束 ## 0.1.6 * 添加android 在开始录制时进行权限验证判断 ## 0.1.5 * 实现根据传递的路径进行语音录制 ## 0.1.4 * 解决 android 申请权限失败问题 * 解决 快速点击发送语音录制停止不了问题 ## 0.1.3 * 实现播放完成的回调监听 ## 0.1.2 * 实现播放指定路径录音文件 ## 0.1.1 * 格式代码 ## 0.1.0 * 修复提示bug ## 0.0.9 * 添加记录录制时间的功能 ## 0.0.8 * 重构项目为oc项目 解决播放oc 工程无法使用问题 ## 0.0.7 * 更新readme ## 0.0.6 * 适配android 9.0 * 解决ios集成到oc项目不成功问题 ## 0.0.5 * 添加 readme 说明 ## 0.0.4 * 添加example readme ## 0.0.3 * 添加引入方式 ## 0.0.2 * 添加license ## 0.0.1 * 初始化项目 ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ [TOC] # 使用Flutter实现 仿微信录音的插件 插件支持android 和IOS ------- 插件提供的功能 录制 1. 录制语音, 2. 播放录音, 3. 录制声音大小的监听 4. 提供录制时长的监听 5. 提供类似微信的录制组件 6. 提供播放音频结束的监听 7. 提供根据传递的路径进行语音录制 8. 提供录制wav,mp3格式 具体可参考example 播放 1. 提供播放指定路径的音频文件 2. 提供播放指定Url地址的wav,MP3格式文件 3. 提供播放完成的回调监听 4. 提供暂停和继续播放的功能 5. 提供停止播放的功能 ## 1,引入 在pubspec.yaml 文件上引入如下配置 引入方式1(引入最新的版本) flutter_plugin_record: git: url: https://github.com/yxwandroid/flutter_plugin_record.git 引入方式2 (引入指定某次commit) flutter_plugin_record: git: url: https://github.com/yxwandroid/flutter_plugin_record.git ref: 29c02b15835907879451ad9f8f88c357149c6085 引入方式3 (引入Flutter仓库的library) dependencies: flutter_plugin_record: ^1.0.1 ### 使用 ### 1, 初始化录制 #### 1.1, 初始化录制(wav) 可以在页面初始化的时候进行初始化比如: 在initState方法中进行初始化 //实例化对象 FlutterPluginRecord recordPlugin = new FlutterPluginRecord(); // 初始化 recordPlugin.init() #### 1.2, 初始化录制(Mp3) 可以在页面初始化的时候进行初始化比如: 在initState方法中进行初始化 //实例化对象 FlutterPluginRecord recordPlugin = new FlutterPluginRecord(); // 初始化 recordPlugin.initRecordMp3() ### 2, 开始录制 recordPlugin.start() ### 3, 停止录制 recordPlugin.stop() ### 4, 播放 #### 1,播放 recordPlugin.play() #### 2, 暂停和继续播放 recordPlugin.pausePlay(); #### 3, 停止播放 recordPlugin.stopPlay(); ### 5, 根据传递的路径进行语音录制 recordPlugin.startByWavPath(wavPath); ### 6, 根据传递的路径或则Url进行语音播放 /// /// 参数 path 播放音频的地址 /// ///path 为 url类型对应的在线播放地址 https://linjuli-app-audio.oss-cn-hangzhou.aliyuncs.com/audio/50c39c768b534ce1ba25d837ed153824.wav ///path 对应本地文件路径对应的是本地文件播放肚子 /sdcard/flutterdemo/wiw.wav /// 参数 type /// 当path 为url type为 url /// 当path 为本地地址 type为 file /// Future playByPath(String path, String type) async { return await _invokeMethod('playByPath', { "play": "play", "path": path, "type": type, }); } ### 7, 释放资源 可以在页面退出的时候进行资源释放 比如在 dispose方法中调用如下代码 recordPlugin.dispose() ### 4,回调监听 1,初始化回调监听 ///初始化方法的监听 recordPlugin.responseFromInit.listen((data) { if (data) { print("初始化成功"); } else { print("初始化失败"); } }); 2,开始录制停止录制监听 /// 开始录制或结束录制的监听 recordPlugin.response.listen((data) { if (data.msg == "onStop") { ///结束录制时会返回录制文件的地址方便上传服务器 print("onStop " + data.path); } else if (data.msg == "onStart") { print("onStart --"); } }); 3,录制声音大小回调监听 ///录制过程监听录制的声音的大小 方便做语音动画显示图片的样式 recordPlugin.responseFromAmplitude.listen((data) { var voiceData = double.parse(data.msg); var tempVoice = ""; if (voiceData > 0 && voiceData < 0.1) { tempVoice = "images/voice_volume_2.png"; } else if (voiceData > 0.2 && voiceData < 0.3) { tempVoice = "images/voice_volume_3.png"; } else if (voiceData > 0.3 && voiceData < 0.4) { tempVoice = "images/voice_volume_4.png"; } else if (voiceData > 0.4 && voiceData < 0.5) { tempVoice = "images/voice_volume_5.png"; } else if (voiceData > 0.5 && voiceData < 0.6) { tempVoice = "images/voice_volume_6.png"; } else if (voiceData > 0.6 && voiceData < 0.7) { tempVoice = "images/voice_volume_7.png"; } else if (voiceData > 0.7 && voiceData < 1) { tempVoice = "images/voice_volume_7.png"; } setState(() { voiceIco = tempVoice; if(overlayEntry!=null){ overlayEntry.markNeedsBuild(); } }); print("振幅大小 " + voiceData.toString() + " " + voiceIco); }); 4,播放声音完成的监听监听 recordPlugin.responsePlayStateController.listen((data){ print("播放路径 " + data.playPath ); print("播放状态 " + data.playState ); }); ## 2,录制组件的使用 组件使用效果 android效果 IOS效果 ### 1,在使用的页面进行导入package import 'package:flutter_plugin_record/index.dart'; ### 2,在使用的地方引入VoiceWidget组件 VoiceWidget(), VoiceWidget({startRecord: Function, stopRecord: Function}) { startRecord 开始录制的回调 stopRecord 停止录制的回调 返回的path是录制成功之后文件的保存地址 ## IOS配置注意事项 ### ios集成的的时候需要在info.list添加 NSMicrophoneUsageDescription 打开话筒 NSAppTransportSecurity NSAllowsArbitraryLoads ### ios release 打包失败配置注意事项 ## android配置注意事项 ### android 集成的的时候需要在application标签下添加 tools:replace="android:label" ## TODO * [ ] 双声道切换 单声道切换 ## 感谢 [肖中旺](https://github.com/xzw421771880)对IOS 播放在线Wav的支持 ## 作者的其他开源项目推荐 [基于腾讯云点播封装的flutter版的播放器插件 ](https://github.com/yxwandroid/flutter_tencentplayer_plus) [Flutter 二维码扫描插件](https://github.com/yxwandroid/flutter_plugin_qrcode) [抖音开发平台SDK Flutter插件](https://github.com/yxwandroid/flutter_plugin_douyin_open) [FLutter地图插件](https://github.com/yxwandroid/flutter_amap_location) [Flutter 模板工程](https://github.com/yxwandroid/flutter_app_redux.git) ## 关注公众号获取更多内容 ================================================ FILE: android/.gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures ================================================ FILE: android/build.gradle ================================================ group 'record.wilson.flutter.com.flutter_plugin_record' version '1.0-SNAPSHOT' buildscript { // ext.kotlin_version = '1.2.71' ext.kotlin_version = '1.3.50' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } rootProject.allprojects { repositories { google() jcenter() maven { url 'https://jitpack.io' } } } apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { compileSdkVersion 29 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { minSdkVersion 19 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { disable 'InvalidPackage' } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.github.shaoshuai904:RecordWav:1.0.2' implementation "androidx.appcompat:appcompat:1.0.0" implementation 'com.github.adrielcafe:AndroidAudioConverter:0.0.8' } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M ================================================ FILE: android/settings.gradle ================================================ rootProject.name = 'flutter_plugin_record' ================================================ FILE: android/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/FlutterPluginRecordPlugin.kt ================================================ package record.wilson.flutter.com.flutter_plugin_record import android.Manifest import android.app.Activity import android.content.Context import android.content.pm.PackageManager import android.util.Log import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import cafe.adriel.androidaudioconverter.AndroidAudioConverter import cafe.adriel.androidaudioconverter.callback.IConvertCallback import cafe.adriel.androidaudioconverter.callback.ILoadCallback import cafe.adriel.androidaudioconverter.model.AudioFormat import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry import io.flutter.plugin.common.PluginRegistry.Registrar import record.wilson.flutter.com.flutter_plugin_record.utils.* import java.io.File import java.util.* class FlutterPluginRecordPlugin : FlutterPlugin, MethodCallHandler, ActivityAware ,PluginRegistry.RequestPermissionsResultListener { lateinit var channel: MethodChannel private lateinit var _result: Result private lateinit var call: MethodCall private lateinit var voicePlayPath: String private var recorderUtil: RecorderUtil? = null private var recordMp3:Boolean=false; @Volatile private var audioHandler: AudioHandler? = null lateinit var activity:Activity companion object { //support embedding v1 @JvmStatic fun registerWith(registrar: Registrar) { val plugin = initPlugin(registrar.messenger()) plugin.activity=registrar.activity() registrar.addRequestPermissionsResultListener(plugin) } private fun initPlugin(binaryMessenger: BinaryMessenger):FlutterPluginRecordPlugin { val channel = createMethodChannel(binaryMessenger) val plugin = FlutterPluginRecordPlugin() channel.setMethodCallHandler(plugin) plugin.channel = channel return plugin } private fun createMethodChannel(binaryMessenger: BinaryMessenger):MethodChannel{ return MethodChannel(binaryMessenger, "flutter_plugin_record"); } } override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { val methodChannel = createMethodChannel(binding.binaryMessenger) methodChannel.setMethodCallHandler(this) channel=methodChannel } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { } override fun onAttachedToActivity(binding: ActivityPluginBinding) { initActivityBinding(binding) } private fun initActivityBinding(binding: ActivityPluginBinding) { binding.addRequestPermissionsResultListener(this) activity=binding.activity } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { initActivityBinding(binding) } override fun onDetachedFromActivityForConfigChanges() { } override fun onDetachedFromActivity() { } override fun onMethodCall(call: MethodCall, result: Result) { _result = result this.call = call when (call.method) { "init" -> init() "initRecordMp3" -> initRecordMp3() "start" -> start() "startByWavPath" -> startByWavPath() "stop" -> stop() "play" -> play() "pause" -> pause() "playByPath" -> playByPath() "stopPlay" -> stopPlay() else -> result.notImplemented() } } //初始化wav转 MP3 private fun initWavToMp3(){ AndroidAudioConverter.load(activity.applicationContext, object : ILoadCallback { override fun onSuccess() { // Great! Log.d("android", " AndroidAudioConverter onSuccess") } override fun onFailure(error: Exception) { // FFmpeg is not supported by device Log.d("android", " AndroidAudioConverter onFailure") } }) } private fun initRecord() { if (audioHandler != null) { audioHandler?.release() audioHandler = null } audioHandler = AudioHandler.createHandler(AudioHandler.Frequency.F_22050) Log.d("android voice ", "init") val id = call.argument("id") val m1 = HashMap() m1["id"] = id!! m1["result"] = "success" channel.invokeMethod("onInit", m1) } private fun stopPlay() { recorderUtil?.stopPlay() } //暂停播放 private fun pause() { val isPlaying= recorderUtil?.pausePlay() val _id = call.argument("id") val m1 = HashMap() m1["id"] = _id!! m1["result"] = "success" m1["isPlaying"] = isPlaying.toString() channel.invokeMethod("pausePlay", m1) } private fun play() { recorderUtil = RecorderUtil(voicePlayPath) recorderUtil!!.addPlayStateListener { playState -> print(playState) val _id = call.argument("id") val m1 = HashMap() m1["id"] = _id!! m1["playPath"] = voicePlayPath m1["playState"] = playState.toString() channel.invokeMethod("onPlayState", m1) } recorderUtil!!.playVoice() Log.d("android voice ", "play") val _id = call.argument("id") val m1 = HashMap() m1["id"] = _id!! channel.invokeMethod("onPlay", m1) } private fun playByPath() { val path = call.argument("path") recorderUtil = RecorderUtil(path) recorderUtil!!.addPlayStateListener { playState -> val _id = call.argument("id") val m1 = HashMap() m1["id"] = _id!! m1["playPath"] = path.toString(); m1["playState"] = playState.toString() channel.invokeMethod("onPlayState", m1) } recorderUtil!!.playVoice() Log.d("android voice ", "play") val _id = call.argument("id") val m1 = HashMap() m1["id"] = _id!! channel.invokeMethod("onPlay", m1) } @Synchronized private fun stop() { if (audioHandler != null) { if (audioHandler?.isRecording == true) { audioHandler?.stopRecord() } } Log.d("android voice ", "stop") } @Synchronized private fun start() { var packageManager = activity.packageManager var permission = PackageManager.PERMISSION_GRANTED == packageManager.checkPermission(Manifest.permission.RECORD_AUDIO,activity.packageName) if (permission) { Log.d("android voice ", "start") // recorderUtil.startRecord(); if (audioHandler?.isRecording == true) { // audioHandler?.startRecord(null); audioHandler?.stopRecord() } audioHandler?.startRecord(MessageRecordListener()) val _id = call.argument("id") val m1 = HashMap() m1["id"] = _id!! m1["result"] = "success" channel.invokeMethod("onStart", m1) } else { checkPermission() } } @Synchronized private fun startByWavPath() { var packageManager = activity.packageManager var permission = PackageManager.PERMISSION_GRANTED == packageManager.checkPermission(Manifest.permission.RECORD_AUDIO, activity.packageName) if (permission) { Log.d("android voice ", "start") val _id = call.argument("id") val wavPath = call.argument("wavPath") if (audioHandler?.isRecording == true) { audioHandler?.stopRecord() } audioHandler?.startRecord(wavPath?.let { MessageRecordListenerByPath(it) }) val m1 = HashMap() m1["id"] = _id!! m1["result"] = "success" channel.invokeMethod("onStart", m1) } else { checkPermission() } } private fun init() { recordMp3=false checkPermission() } private fun initRecordMp3(){ recordMp3=true checkPermission() initWavToMp3() } private fun checkPermission() { var packageManager = activity.packageManager var permission = PackageManager.PERMISSION_GRANTED == packageManager.checkPermission(Manifest.permission.RECORD_AUDIO,activity.packageName) if (permission) { initRecord() } else { initPermission() } } private fun initPermission() { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) !== PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE), 1) } } //自定义路径 private inner class MessageRecordListenerByPath : AudioHandler.RecordListener { var wavPath = "" constructor(wavPath: String) { this.wavPath = wavPath } override fun onStop(recordFile: File?, audioTime: Double?) { if (recordFile != null) { voicePlayPath = recordFile.path if (recordMp3){ val callback: IConvertCallback = object : IConvertCallback { override fun onSuccess(convertedFile: File) { Log.d("android", " ConvertCallback ${convertedFile.path}") val _id = call.argument("id") val m1 = HashMap() m1["id"] = _id!! m1["voicePath"] = convertedFile.path m1["audioTimeLength"] = audioTime.toString() m1["result"] = "success" activity.runOnUiThread { channel.invokeMethod("onStop", m1) } } override fun onFailure(error: java.lang.Exception) { Log.d("android", " ConvertCallback $error") } } AndroidAudioConverter.with(activity.applicationContext) .setFile(recordFile) .setFormat(AudioFormat.MP3) .setCallback(callback) .convert() }else{ val _id = call.argument("id") val m1 = HashMap() m1["id"] = _id!! m1["voicePath"] = voicePlayPath m1["audioTimeLength"] = audioTime.toString() m1["result"] = "success" activity.runOnUiThread { channel.invokeMethod("onStop", m1) } } } } override fun getFilePath(): String { return wavPath; } private val fileName: String private val cacheDirectory: File init { cacheDirectory = FileTool.getIndividualAudioCacheDirectory(activity) fileName = UUID.randomUUID().toString() } override fun onStart() { LogUtils.LOGE("MessageRecordListener onStart on start record") } override fun onVolume(db: Double) { LogUtils.LOGE("MessageRecordListener onVolume " + db / 100) val _id = call.argument("id") val m1 = HashMap() m1["id"] = _id!! m1["amplitude"] = db / 100 m1["result"] = "success" activity.runOnUiThread { channel.invokeMethod("onAmplitude", m1) } } override fun onError(error: Int) { LogUtils.LOGE("MessageRecordListener onError $error") } } private inner class MessageRecordListener : AudioHandler.RecordListener { override fun onStop(recordFile: File?, audioTime: Double?) { LogUtils.LOGE("MessageRecordListener onStop $recordFile") if (recordFile != null) { voicePlayPath = recordFile.path if (recordMp3){ val callback: IConvertCallback = object : IConvertCallback { override fun onSuccess(convertedFile: File) { Log.d("android", " ConvertCallback ${convertedFile.path}") val _id = call.argument("id") val m1 = HashMap() m1["id"] = _id!! m1["voicePath"] = convertedFile.path m1["audioTimeLength"] = audioTime.toString() m1["result"] = "success" activity.runOnUiThread { channel.invokeMethod("onStop", m1) } } override fun onFailure(error: java.lang.Exception) { Log.d("android", " ConvertCallback $error") } } AndroidAudioConverter.with(activity.applicationContext) .setFile(recordFile) .setFormat(AudioFormat.MP3) .setCallback(callback) .convert() }else{ val _id = call.argument("id") val m1 = HashMap() m1["id"] = _id!! m1["voicePath"] = voicePlayPath m1["audioTimeLength"] = audioTime.toString() m1["result"] = "success" activity.runOnUiThread { channel.invokeMethod("onStop", m1) } } } } override fun getFilePath(): String { val file = File(cacheDirectory, fileName) return file.absolutePath } private val fileName: String private val cacheDirectory: File init { cacheDirectory = FileTool.getIndividualAudioCacheDirectory(activity) fileName = UUID.randomUUID().toString() } override fun onStart() { LogUtils.LOGE("MessageRecordListener onStart on start record") } override fun onVolume(db: Double) { LogUtils.LOGE("MessageRecordListener onVolume " + db / 100) val _id = call.argument("id") val m1 = HashMap() m1["id"] = _id!! m1["amplitude"] = db / 100 m1["result"] = "success" activity.runOnUiThread { channel.invokeMethod("onAmplitude", m1) } } override fun onError(error: Int) { LogUtils.LOGE("MessageRecordListener onError $error") } } // 权限监听回调 override fun onRequestPermissionsResult(p0: Int, p1: Array?, p2: IntArray?): Boolean { if (p0 == 1) { if (p2?.get(0) == PackageManager.PERMISSION_GRANTED) { // initRecord() return true } else { Toast.makeText(activity, "Permission Denied", Toast.LENGTH_SHORT).show() DialogUtil.Dialog(activity, "申请权限") } return false } return false } } ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/timer/ITimer.java ================================================ package record.wilson.flutter.com.flutter_plugin_record.timer; public interface ITimer { void startTimer(); void pauseTimer(); void resumeTimer(); void stopTimer(); } ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/timer/ITimerChangeCallback.java ================================================ package record.wilson.flutter.com.flutter_plugin_record.timer; public interface ITimerChangeCallback { void onTimeChange(long time); } ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/timer/MTimer.java ================================================ package record.wilson.flutter.com.flutter_plugin_record.timer; import android.text.TextUtils; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicLong; public class MTimer implements ITimer { //--real timer-- private Timer timer; //--default task-- private TimerTask task; //--default initdelay-- private long initDelay = 0l; //--default delay-- private long delay = 0l; //--call back-- private ITimerChangeCallback[] callbacks = null; //--real time-- private AtomicLong time;//时间的记录工具 //--action-- private static final int START = 0; private static final int PAUSE = 1; private static final int RESUME = 2; private static final int STOP = 3; private int status = STOP;//默认是stop private MTimer(long initDelay, long delay, ITimerChangeCallback[] callbacks) { this.initDelay = initDelay; this.delay = delay; this.callbacks = callbacks; } //-----------------外部方法------------------------ /** * 用于生成MTimer 对象 * * @return --MTimer */ public static Builder makeTimerBuilder() { return new Builder(); } /** * 开启 timer */ @Override public void startTimer() { //判断当前是不是stop,是的话开始运行 if (status != STOP) { return; } //切换当前状态为 start status = START; realStartTimer(true); } /** * 暂停timer */ @Override public void pauseTimer() { //判断当前是不是start 是不是resume,如果是其中一个就可以 if (status != START && status != RESUME) { return; } //切换当前状态为 pause status = PAUSE; realStopTimer(false); } /** * 重启timer */ @Override public void resumeTimer() { //判断当前是不是pause ,如果是则恢复 if (status != PAUSE) { return; } //切换当前状态为 resume status = RESUME; realStartTimer(false); } /** * 关闭timer */ @Override public void stopTimer() { //无论当前处于那种状态都可以stop status = STOP; realStopTimer(true); } //-----------------内部方法------------------------ /** * timer 真正的开始方法 * * @param isToZero --是否清除数据 */ private void realStartTimer(boolean isToZero) { //清空记录时间 if (isToZero) { time = new AtomicLong(0); } //重新生成timer、task if (timer == null && task == null) { timer = new Timer(); task = createTask(); timer.scheduleAtFixedRate(task, initDelay, delay); } } /** * timer 真正的关闭方法 * * @param isToZero --是否清除数据 */ private void realStopTimer(boolean isToZero) { //清空记录时间 if (isToZero) { time = new AtomicLong(0); } //关闭当前的timer if (timer != null) { timer.purge(); timer.cancel(); timer = null; } //关闭当前任务 if (task != null) { task.cancel(); task = null; } } /** * 判断是否设置监听回调 * * @return -- true 表示设置了回调,反之表示没设置 */ private boolean checkCallback() { return callbacks != null && callbacks.length > 0; } /** * 创建task * * @return */ private TimerTask createTask() { TimerTask task = new TimerTask() { @Override public void run() { time.incrementAndGet(); notifyCallback(time); } }; return task; } /** * 通知callback * * @param time --间距走的次数(花费时间=次数*delay+initDelay) */ private void notifyCallback(AtomicLong time) { if (checkCallback()) { for (ITimerChangeCallback callback : callbacks) { callback.onTimeChange(time.longValue()); } } } public static class Builder { //--default initdelay-- private long initDelay = 0l; //--default delay-- private long delay = 0l; //--call back-- private ITimerChangeCallback[] callbacks = null; //--tag-- private String tag; public Builder setTag(String tag) { if (TextUtils.isEmpty(tag)) { throw new NullPointerException("设置的tag无效!=>setTag(String tag)"); } this.tag = tag; return this; } /** * 设置执行当前任务的时候首次执行时的延迟时间 * * @param initDelay --首次执行的延迟时间(ms) */ public Builder setInitDelay(long initDelay) { this.initDelay = initDelay; return this; } /** * 设置时间回调 * * @param callbacks */ public Builder setCallbacks(ITimerChangeCallback... callbacks) { this.callbacks = callbacks; return this; } /** * 设置后续的延迟时间 * * @param delay --后续延迟时间(ms) */ public Builder setDelay(long delay) { this.delay = delay; return this; } /** * 外部会重用此对象,所以需要重置其参数 */ public void reset() { tag = null; initDelay = 0l; delay = 0l; callbacks = null; } /** * 最终的生成方法,如果不调用此处,timer无法运行 */ public MTimer build() { //--check delay-- if (initDelay < 0 || delay < 0) { throw new AssertionError("initDelay或delay 不允许小于0"); } //--build timer-- MTimer timer = new MTimer(initDelay, delay, callbacks); //--add to cache-- if (!TextUtils.isEmpty(tag)) { TimerUtils.addTimerToCache(tag, timer); } //--return timer-- return timer; } } } ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/timer/TimerUtils.java ================================================ package record.wilson.flutter.com.flutter_plugin_record.timer; import android.text.TextUtils; import android.util.Log; import java.util.Locale; import java.util.WeakHashMap; /** * description: 定时器工具类 * author: Simon * created at 2017/8/10 上午9:47 */ public final class TimerUtils { //--tag-- private static final String TAG = "TimerUtils"; //--err info-- private static final String ERR_INFO = "未找到对应的MTimer,确认是否设置过Tag!=>new Builder().setTag(String tag)"; //--cache-- private static WeakHashMap cacheTimerMap = new WeakHashMap<>(); //--action-- private static final int START = 0; private static final int PAUSE = 1; private static final int RESUME = 2; private static final int STOP = 3; //--recycle build-- private static final MTimer.Builder BUILDER = new MTimer.Builder(); private TimerUtils() { throw new AssertionError("you can't init me!"); } /** * 注意此方法,会重复利用Builder 对象,所以每次build()完成后再重新使用该方法!! * * @return --builder */ public static MTimer.Builder makeBuilder() { BUILDER.reset();//每次执行的时候都会重置一次 return BUILDER; } /** * 开启timer ,时间清零 */ public static void startTimer(String tag) { actionTimer(START, tag); } /** * 恢复timer,不清零 */ public static void resumeTimer(String tag) { actionTimer(RESUME, tag); } /** * 暂停timer */ public static void pauseTimer(String tag) { actionTimer(PAUSE, tag); } /** * 关闭 timer */ public static void stopTimer(String tag) { actionTimer(STOP, tag); } /** * 格式化 时间 格式为 hh:mm:ss * * @param cnt * @return */ public static String formatTime(long cnt) { long hour = cnt / 3600; long min = cnt % 3600 / 60; long second = cnt % 60; return String.format(Locale.CHINA, "%02d:%02d:%02d", hour, min, second); } //------------------------私有方法/内部类------------------------------ /** * 添加timer到缓存 * * @param tag --tag * @param timer --timer */ public static void addTimerToCache(String tag, MTimer timer) { if (cacheTimerMap == null) { cacheTimerMap = new WeakHashMap<>(); } cacheTimerMap.put(tag, timer); } /** * 真正的执行方法 * * @param action --行为 * @param tag --tag */ private static void actionTimer(int action, String tag) { //-----check tag---- if (!checkTag(tag)) { Log.e(TAG, "The tag is empty or null!"); return; } //-----check timer---- MTimer timer = findMTimerByTag(tag); if (timer == null) { Log.e(TAG, "Can't found timer by tag!"); return; } //-----action timer---- switch (action) { case START: timer.startTimer(); break; case RESUME: timer.resumeTimer(); break; case PAUSE: timer.pauseTimer(); break; case STOP: timer.stopTimer(); break; } } /** * 通过tag获取mtimer * * @param tag --设置的tag * @return --MTimer */ private static MTimer findMTimerByTag(String tag) { if (!checkTag(tag) || cacheTimerMap == null || cacheTimerMap.size() == 0) {//tag无效,没有缓存数据,返回null return null; } else {//反之根据tag返回 return cacheTimerMap.get(tag); } } /** * 判断tag 是否有效 * * @param tag --tag * @return true表示有效,反之无效 */ private static boolean checkTag(String tag) { return !TextUtils.isEmpty(tag); } } ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/AudioHandler.java ================================================ package record.wilson.flutter.com.flutter_plugin_record.utils; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaRecorder; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Process; import android.util.Log; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; import java.lang.ref.WeakReference; import java.text.SimpleDateFormat; import java.util.Locale; import record.wilson.flutter.com.flutter_plugin_record.timer.ITimerChangeCallback; import record.wilson.flutter.com.flutter_plugin_record.timer.TimerUtils; public final class AudioHandler extends Handler { private static final String TAG = "AudioHandler"; private final WeakReference mThread; private static final int MESSAGE_START_RECORD = 0X01; private static final int MESSAGE_PAUSE_RECORD = 0X02; private static final int MESSAGE_STOP_RECORD = 0X03; private static final int MESSAGE_SAVE_RECORD = 0X04; private static final int MESSAGE_ADD_LISTENER = 0X05; private static final int MESSAGE_REMOVE_LISTENER = 0X06; private static final int MESSAGE_RELEASE = 0X07; private static final int MESSAGE_GET_LATEST_RECORD = 0X08; public static final int MAX_DB = 96; public static final int STATE_AUDIO_RECORD_PREPARING = 0X01; public static final int STATE_AUDIO_RECORD_START = 0X02; public static final int STATE_AUDIO_RECORD_PAUSE = 0X03; public static final int STATE_AUDIO_RECORD_STOPPED = 0X04; public static AudioHandler createHandler(Frequency frequency) { AudioThread thread = new AudioThread(frequency); thread.start(); return thread.getHandler(); } private AudioHandler(AudioThread thread) { mThread = new WeakReference(thread); } public void startRecord(RecordListener listener) { if (isRecording()) stopRecord(); Message message = obtainMessage(MESSAGE_START_RECORD); message.obj = listener; sendMessage(message); } public boolean isAvailable() { AudioThread audioThread = mThread.get(); return audioThread != null && audioThread.isAvailable; } public void stopRecord() { AudioThread audioThread = mThread.get(); if (audioThread != null) audioThread.setPauseRecord(); } public void cancelRecord() { AudioThread audioThread = mThread.get(); if (audioThread != null) audioThread.setCancelRecord(); } public boolean isRecording() { AudioThread audioThread = mThread.get(); return audioThread != null && audioThread.isRecording(); } public void release() { AudioThread audioThread = mThread.get(); if (audioThread != null) audioThread.release(); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); AudioThread audioThread = mThread.get(); if (audioThread == null) return; switch (msg.what) { case MESSAGE_START_RECORD: Object obj = msg.obj; RecordListener listener = null; if (obj instanceof RecordListener) listener = (RecordListener) obj; audioThread.startRecord(listener); break; default: break; } } public enum Frequency { F_44100(44100), F_22050(22050), F_16000(16000), F_11025(11025), F_8000(8000); private int f; private Frequency(int f) { this.f = f; } public int getFrequency() { return f; } } private static final class AudioThread extends Thread { private static final int[] FREQUENCY = { 44100, 22050, 16000, 11025, 8000 }; private final int mPriority; private AudioHandler mHandler; private boolean isAvailable; private final Object sync = new Object(); private int mFrequency; private int channel = AudioFormat.CHANNEL_IN_MONO; private int audioFormat = AudioFormat.ENCODING_PCM_16BIT; private int bufferSize; private AudioRecord mRecord; private SimpleDateFormat format; private int mTid; private Looper mLooper; private volatile boolean isCancel; private String tag = "AudioTimerTag"; private double audioTime = 0; //录音时长 //--设置 tag 后可以通过 tag 操作-- private void initTimer() { TimerUtils.makeBuilder().setTag(tag).setInitDelay(0).setDelay(100).setCallbacks(new ITimerChangeCallback() { @Override public void onTimeChange(long time) { //Log.v("AudioTimerTag", time + "--> AudioTimer"); audioTime = time / 10.0; } }).build(); } private AudioThread(Frequency frequency) { mPriority = Process.THREAD_PRIORITY_DEFAULT; mFrequency = frequency.getFrequency(); isAvailable = checkSampleRatesValid(mFrequency); Log.e(TAG, "FREQUENCY " + mFrequency); format = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()); if (isAvailable) { bufferSize = AudioRecord.getMinBufferSize(mFrequency, channel, audioFormat); Log.e(TAG, String.format("buffer size %d", bufferSize)); } initTimer(); } boolean isRecording() { if (mRecord == null) { return false; } if (mRecord.getState() == AudioRecord.STATE_UNINITIALIZED) { return false; } return mRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING; } private volatile boolean isRecording; private void startRecord(RecordListener listener) { audioTime =0; TimerUtils.startTimer(tag); Log.e(TAG, "call start record"); if (!isAvailable) { return; } byte[] buffer = new byte[bufferSize]; pauseRecord(); if (mRecord == null) { mRecord = new AudioRecord(MediaRecorder.AudioSource.MIC , mFrequency , channel , audioFormat , bufferSize * 10); } if (mRecord.getState() != AudioRecord.STATE_INITIALIZED) { pauseRecord(); return; } BufferedOutputStream out = null; File recordFile = null; if (listener != null) { String filePath = listener.getFilePath(); if (!filePath.endsWith(".wav")) filePath = filePath + ".wav"; recordFile = new File(filePath); } try { if (listener != null) { if (!recordFile.createNewFile()) { listener.onError(-20); pauseRecord(); return; } out = new BufferedOutputStream(new FileOutputStream(recordFile)); WaveHeaderHelper.writeHeader(out, mFrequency, 16, 1); } mRecord.startRecording(); isCancel = false; if (listener != null) listener.onStart(); Log.e(TAG, "start recording"); isRecording = true; // Log.d(TAG, "BUFFER LIMIT IS " + buffer.limit() + "\n\t\t\tCAPACITY IS" + buffer.capacity()); long length = 0; boolean turn = false; while (isRecording && mRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { int read = mRecord.read(buffer, 0, buffer.length); if (read < 0) { Log.e(TAG, read == -3 ? "ERROR_INVALID_OPERATION" : read == -2 ? "ERROR_BAD_VALUE" : read == -1 ? "ERROR" : String.valueOf(read)); if (listener != null) listener.onError(read); break; } if (out != null) { out.write(buffer, 0, read); } if (listener != null) { if (turn) { listener.onVolume(getDb(buffer)); } turn = !turn; } length += read; } } catch (Throwable t) { t.printStackTrace(); } finally { pauseRecord(); FileTool.closeIO(out); } if (listener != null) { WaveHeaderHelper.writeWaveHeaderLength(recordFile); if (isCancel) { recordFile.deleteOnExit(); recordFile = null; } TimerUtils.stopTimer(tag); // listener.onStop(recordFile); listener.onStop(recordFile, audioTime); } } public void setPauseRecord() { isRecording = false; } public void setCancelRecord() { isCancel = true; setPauseRecord(); } private double getDb(byte[] buffer) { double diviation = getDiviation(buffer, 0, buffer.length); return 20 * Math.log10(diviation); } private static short getShort(byte argB1, byte argB2) { //if (ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN)) { // return (short) ((argB1 << 8) | argB2); //} return (short) (argB1 | (argB2 << 8)); } private static double getDiviation(byte[] buffer, int start, int length) { if (0 != (length % 2)) { length--; } double[] divArray = new double[length]; // short[] array = ByteBuffer.wrap(buffer, start, length).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().array(); for (int i = start; i < start + length; i += 2) { divArray[i / 2] = getShort(buffer[i], buffer[i + 1]) * 1.0; } return StandardDiviation(divArray); } //标准差σ=sqrt(s^2) private static double StandardDiviation(double[] x) { int m = x.length; double sum = 0; for (int i = 0; i < m; i++) {//求和 sum += x[i]; } double dAve = sum / m;//求平均值 double dVar = 0; for (int i = 0; i < m; i++) {//求方差 double v = x[i] - dAve; dVar += v * v; } return Math.sqrt(dVar / m); } private void pauseRecord() { setPauseRecord(); if (mRecord != null) { if (mRecord.getState() == AudioRecord.STATE_UNINITIALIZED) { mRecord.release(); mRecord = null; } if (mRecord != null && mRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { mRecord.stop(); mRecord.release(); mRecord = null; } } } public void release() { pauseRecord(); synchronized (sync) { if (Looper.myLooper() != Looper.getMainLooper()) { if (mLooper != null) { mLooper.quit(); } } } } @Override public void run() { mTid = Process.myTid(); Log.e(TAG, "thread start running"); Looper.prepare(); synchronized (sync) { mLooper = Looper.myLooper(); mHandler = new AudioHandler(this); sync.notifyAll(); } Process.setThreadPriority(mPriority); Looper.loop(); synchronized (sync) { mHandler = null; sync.notifyAll(); } mTid = -1; } AudioHandler getHandler() { if (!isAlive()) { return null; } synchronized (sync) { while (isAlive() && mHandler == null) { try { sync.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } return mHandler; } public boolean checkSampleRatesValid(int frequency) { int bufferSize = AudioRecord.getMinBufferSize(frequency, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); return bufferSize > 0; } public int getValidSampleRates() { for (int i = 0; i < FREQUENCY.length; i++) { int bufferSize = AudioRecord.getMinBufferSize(FREQUENCY[i], AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); if (bufferSize > 0) { return FREQUENCY[i]; } } return -1; } } public interface RecordListener { void onStart(); String getFilePath(); void onVolume(double db); // void onStop(File recordFile); void onStop(File recordFile,Double audioTime); void onError(int error); } private static class WaveHeaderHelper { private static void writeHeader(OutputStream out, int sampleRate, int encoding, int channel) throws IOException { writeString(out, "RIFF"); // chunk id writeInt(out, 0); // chunk size writeString(out, "WAVE"); // format writeString(out, "fmt "); // subchunk 1 id writeInt(out, 16); // subchunk 1 size writeShort(out, (short) 1); // audio format (1 = PCM) writeShort(out, (short) channel); // number of channels writeInt(out, sampleRate); // sample rate writeInt(out, sampleRate * channel * encoding >> 3); // byte rate writeShort(out, (short) (channel * encoding >> 3)); // block align writeShort(out, (short) encoding); // bits per sample writeString(out, "data"); // subchunk 2 id writeInt(out, 0); // subchunk 2 size } private static void writeWaveHeaderLength(File f) { RandomAccessFile raf = null; try { raf = new RandomAccessFile(f, "rw"); long length = f.length(); long chunkSize = length - 8; long subChunkSize = length - 44; raf.seek(4); raf.write((int) (chunkSize >> 0)); raf.write((int) (chunkSize >> 8)); raf.write((int) (chunkSize >> 16)); raf.write((int) (chunkSize >> 24)); raf.seek(40); raf.write((int) (subChunkSize >> 0)); raf.write((int) (subChunkSize >> 8)); raf.write((int) (subChunkSize >> 16)); raf.write((int) (subChunkSize >> 24)); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { FileTool.closeIO(raf); } } private static void writeInt(final OutputStream output, final int value) throws IOException { output.write(value >> 0); output.write(value >> 8); output.write(value >> 16); output.write(value >> 24); } private static void writeShort(final OutputStream output, final short value) throws IOException { output.write(value >> 0); output.write(value >> 8); } private static void writeString(final OutputStream output, final String value) throws IOException { for (int i = 0; i < value.length(); i++) { output.write(value.charAt(i)); } } } } ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/DateUtils.java ================================================ package record.wilson.flutter.com.flutter_plugin_record.utils; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtils { public static String dateToString(Date date) { String str = "yyyyMMddhhmmss"; SimpleDateFormat format = new SimpleDateFormat(str); String dateFormat = format.format(date); return dateFormat; } } ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/DialogUtil.java ================================================ package record.wilson.flutter.com.flutter_plugin_record.utils; import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.app.AlertDialog; /** * 对话框管理 * 打开app应用程序信息界面 */ public class DialogUtil { public static void Dialog(final Activity activity, String content) { Dialog deleteDialog = new AlertDialog.Builder(activity) .setTitle("提示") .setMessage("请进入应用信息界面开启录音权限") .setPositiveButton("确定", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { startSetting(activity); } }) .create(); deleteDialog.setCanceledOnTouchOutside(false); deleteDialog.setCancelable(false); deleteDialog.show(); } /** * 启动app设置应用程序信息界面 */ public static void startSetting(Context context) { Intent intent = new Intent(); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); if (Build.VERSION.SDK_INT >= 9) { intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS"); intent.setData(Uri.fromParts("package", context.getPackageName(), null)); } else if (Build.VERSION.SDK_INT <= 8) { intent.setAction(Intent.ACTION_VIEW); intent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails"); intent.putExtra(context.getPackageName(), context.getPackageName()); } context.startActivity(intent); } } ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/FileTool.java ================================================ package record.wilson.flutter.com.flutter_plugin_record.utils; import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageManager; import android.media.ExifInterface; import android.os.Build; import android.os.Environment; import android.os.StatFs; import android.text.TextUtils; import android.webkit.MimeTypeMap; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.net.FileNameMap; import java.net.URLConnection; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.regex.Matcher; import java.util.regex.Pattern; import static android.os.Environment.MEDIA_MOUNTED; public final class FileTool { private FileTool() { } public static final String FILE_TYPE_PDF = "pdf"; public static final String FILE_TYPE_APK = "vnd.android.package-archive"; public static final String FILE_TYPE_EXCEL = "ms-excel"; public static final String FILE_TYPE_EXCELX = "vnd.openxmlformats-officedocument.spreadsheetml"; public static final String FILE_TYPE_PPT = "powerpoint"; public static final String FILE_TYPE_PPTX = "vnd.openxmlformats-officedocument.presentationml"; public static final String FILE_TYPE_WORD = "word"; public static final String FILE_TYPE_WORDX = "vnd.openxmlformats-officedocument.wordprocessingml"; public static final String FILE_TYPE_RAR = "rar"; public static final String FILE_TYPE_ZIP = "zip"; public static final String FILE_TYPE_AUDIO = "audio"; public static final String FILE_TYPE_TEXT = "text"; public static final String FILE_TYPE_XML = "xml"; public static final String FILE_TYPE_HTML = "html"; public static final String FILE_TYPE_IMAGE = "image"; public static final String FILE_TYPE_VIDEO = "video"; public static final String FILE_TYPE_APP = "application"; /** * @Fields maxFileSize : 最大允许文件大小 **/ private static final int MAX_FILE_SIZE = 2 * 1024 * 1024; public static byte[] GetFileDataBytes(File file, int fileLen) { ByteArrayOutputStream bos = null; BufferedInputStream in = null; try { bos = new ByteArrayOutputStream(fileLen); in = new BufferedInputStream(new FileInputStream(file)); int buf_size = 1024; byte[] buffer = new byte[buf_size]; int len = 0; while (-1 != (len = in.read(buffer, 0, buf_size))) { bos.write(buffer, 0, len); } return bos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { closeIO(bos, in); } return null; } public static String getFileRootPathString(Context context) { return new String(getFileRootPath(context).getAbsolutePath()); } public static String getDefaultApkSavePath() { return new StringBuffer(Environment.getExternalStorageDirectory().getAbsolutePath()).append(File.separator).append(Environment.DIRECTORY_DOWNLOADS).toString(); } public static File getFileRootPath(Context context) { File file = null; if (context == null) return null; if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { file = context.getExternalCacheDir(); } if (file == null) { file = context.getCacheDir(); } return file; } // public static String getHeadImagePathString(Context mContext) { // return getHeadImagePath(mContext).getAbsolutePath(); // } public static void copyFile2(String oldFilePath, String newFilePath) { FileOutputStream fs = null; FileInputStream inStream = null; try { fs = new FileOutputStream(newFilePath); inStream = new FileInputStream(oldFilePath); int byteread = 0; if (inStream.available() <= MAX_FILE_SIZE) { byte[] buffer = new byte[1024]; while ((byteread = inStream.read(buffer)) != -1) { fs.write(buffer, 0, byteread); } } } catch (Exception e) { e.printStackTrace(); } finally { closeIO(fs, inStream); } } public static boolean isValidName(String text) { Pattern pattern = Pattern.compile( "# Match a valid Windows filename (unspecified file system). \n" + "^ # Anchor to start of string. \n" + "(?! # Assert filename is not: CON, PRN, \n" + " (?: # AUX, NUL, COM1, COM2, COM3, COM4, \n" + " CON|PRN|AUX|NUL| # COM5, COM6, COM7, COM8, COM9, \n" + " COM[1-9]|LPT[1-9] # LPT1, LPT2, LPT3, LPT4, LPT5, \n" + " ) # LPT6, LPT7, LPT8, and LPT9... \n" + " (?:\\.[^.]*)? # followed by optional extension \n" + " $ # and end of string \n" + ") # End negative lookahead assertion. \n" + "[^<>:\"/\\\\|?*\\x00-\\x1F]* # Zero or more valid filename chars.\n" + "[^<>:\"/\\\\|?*\\x00-\\x1F\\ .] # Last char is not a space or dot. \n" + "$ # Anchor to end of string. ", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE | Pattern.COMMENTS); Matcher matcher = pattern.matcher(text); boolean isMatch = matcher.matches(); return isMatch; } public static void deleteFile(File f) { if (f.exists()) { if (f.isDirectory()) { File[] files = f.listFiles(); for (int i = 0; i < files.length; i++) { deleteFile(files[i]); } } f.delete(); } } public static String getFileMD5String(File f) { byte[] buffer = new byte[4 * 1024]; BufferedInputStream bis = null; try { MessageDigest digest = MessageDigest.getInstance("MD5"); bis = new BufferedInputStream(new FileInputStream(f)); int lent; while ((lent = bis.read(buffer)) != -1) { digest.update(buffer, 0, lent); } byte[] hash = digest.digest(); StringBuilder hex = new StringBuilder(hash.length * 2); for (byte b : hash) { if ((b & 0xFF) < 0x10) hex.append("0"); hex.append(Integer.toHexString(b & 0xFF)); } return hex.toString(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } finally { FileTool.closeIO(bis); } return null; } public static String md5(String string) { byte[] hash; try { hash = MessageDigest.getInstance("MD5").digest(string.getBytes("UTF-8")); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Huh, MD5 should be supported?", e); } catch (UnsupportedEncodingException e) { throw new RuntimeException("Huh, UTF-8 should be supported?", e); } StringBuilder hex = new StringBuilder(hash.length * 2); for (byte b : hash) { if ((b & 0xFF) < 0x10) hex.append("0"); hex.append(Integer.toHexString(b & 0xFF)); } return hex.toString(); } /** *

* 判断照片角度 *

* * @param path @return int @throws */ public static int readPictureDegree(String path) { int degree = 0; try { ExifInterface exifInterface = new ExifInterface(path); int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: degree = 90; break; case ExifInterface.ORIENTATION_ROTATE_180: degree = 180; break; case ExifInterface.ORIENTATION_ROTATE_270: degree = 270; break; } } catch (IOException e) { e.printStackTrace(); } return degree; } public static String getFileExtension(File file) { String fileName = file.getName(); return getFileExtension(fileName); } /** * 获取文件扩展名 * * @param fileName * @return */ public static String getFileExtension(String fileName) { if (fileName.lastIndexOf(".") != -1 && fileName.lastIndexOf(".") != 0) { return fileName.substring(fileName.lastIndexOf(".") + 1); } else { return ""; } } public static String urlDelStoken(String url) { url = url.trim().toLowerCase(); // 是否为http并且是否包含Stoken if (!(url.startsWith("http") && url.contains("stoken"))) { return url; } String[] tampStrings = url.split("[?&]"); for (String temp : tampStrings) { if (temp.trim().startsWith("stoken=")) { url = url.replace(temp, "").replace("?&", "?"); break; } } return url; } /******************************************************************************* * Copyright 2011-2014 Sergey Tarasevich *

* Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain a * copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. *******************************************************************************/ private static final String EXTERNAL_STORAGE_PERMISSION = "android.permission.WRITE_EXTERNAL_STORAGE"; private static final String INDIVIDUAL_DIR_NAME = "uil-images"; private static final String INDIVIDUAL_LOG_FOLDER = "logs"; private static final String INDIVIDUAL_UPLOAD_CACHE = "upload-cache"; private static final String FILE_DIR_NAME = "file_cache"; private static final String FILE_AUDIO_DIR_NAME = "Audio"; private static final String FILE_IMAGE_DIR_NAME = "image"; /** * Returns application cache directory. Cache directory will be created on * SD card ("/Android/data/[app_package_name]/cache") if card is * mounted and app has appropriate permission. Else - Android defines cache * directory on device's file system. * * @return Cache {@link File directory}.
* NOTE: Can be null in some unpredictable cases (if SD card * is unmounted and {@link Context#getCacheDir() * Context.getCacheDir()} returns null). */ public static File getCacheDirectory(Context context) { return getCacheDirectory(context, true); } /** * Returns application cache directory. Cache directory will be created on * SD card ("/Android/data/[app_package_name]/cache") (if card is * mounted and app has appropriate permission) or on device's file system * depending incoming parameters. * * @param preferExternal Whether prefer external location for cache * @return Cache {@link File directory}.
* NOTE: Can be null in some unpredictable cases (if SD card * is unmounted and {@link Context#getCacheDir() * Context.getCacheDir()} returns null). */ public static File getCacheDirectory(Context context, boolean preferExternal) { assert context != null; File appCacheDir = null; String externalStorageState; try { externalStorageState = Environment.getExternalStorageState(); } catch (NullPointerException e) { // (sh)it happens (Issue #660) externalStorageState = ""; } catch (IncompatibleClassChangeError e) { // (sh)it happens too (Issue // #989) externalStorageState = ""; } if (preferExternal && MEDIA_MOUNTED.equals(externalStorageState) && hasExternalStoragePermission(context)) { appCacheDir = getExternalCacheDir(context); } if (appCacheDir == null) { appCacheDir = context.getCacheDir(); } if (appCacheDir == null) { String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/"; appCacheDir = new File(cacheDirPath); } return appCacheDir; } /** * Returns individual application cache directory (for only image caching * from ImageLoader). Cache directory will be created on SD card * ("/Android/data/[app_package_name]/cache/uil-images") if card is * mounted and app has appropriate permission. Else - Android defines cache * directory on device's file system. * * @return Cache {@link File directory} */ public static File getIndividualImageCacheDirectory(Context context) { return getIndividualCacheDirectory(context, INDIVIDUAL_DIR_NAME); } public static File getIndividualUploadCacheDirectory(Context context) { return getIndividualCacheDirectory(context, INDIVIDUAL_UPLOAD_CACHE); } public static File getIndividualLogCacheDirectory(Context context) { return getIndividualCacheDirectory(context, INDIVIDUAL_LOG_FOLDER); } public static File getIndividualAudioCacheDirectory(Context context) { File file = new File(getIndividualCacheDirectory(context, FILE_DIR_NAME), FILE_AUDIO_DIR_NAME); if(!file.exists()) file.mkdirs(); return file; } public static File getIndividualImageFileDirectory(Context context) { return new File(getIndividualCacheDirectory(context, FILE_DIR_NAME), FILE_IMAGE_DIR_NAME); } /** * Returns individual application cache directory (for only image caching * from ImageLoader). Cache directory will be created on SD card * ("/Android/data/[app_package_name]/cache/uil-images") if card is * mounted and app has appropriate permission. Else - Android defines cache * directory on device's file system. * * @param cacheDir Cache directory path (e.g.: "AppCacheDir", * "AppDir/cache/images") * @return Cache {@link File directory} */ public static File getIndividualCacheDirectory(Context context, String cacheDir) { File appCacheDir = getCacheDirectory(context); File individualCacheDir = new File(appCacheDir, cacheDir); if (!individualCacheDir.exists()) { if (!individualCacheDir.mkdir()) { individualCacheDir = appCacheDir; } try { new File(individualCacheDir, ".nomedia").createNewFile(); } catch (IOException e) { e.printStackTrace(); } } return individualCacheDir; } /** * Returns specified application cache directory. Cache directory will be * created on SD card by defined path if card is mounted and app has * appropriate permission. Else - Android defines cache directory on * device's file system. * * @param cacheDir Cache directory path (e.g.: "AppCacheDir", * "AppDir/cache/images") * @return Cache {@link File directory} */ public static File getOwnCacheDirectory(Context context, String cacheDir) { File appCacheDir = null; if (MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) && hasExternalStoragePermission(context)) { appCacheDir = new File(Environment.getExternalStorageDirectory(), cacheDir); } if (appCacheDir == null || (!appCacheDir.exists() && !appCacheDir.mkdirs())) { appCacheDir = context.getCacheDir(); } return appCacheDir; } /** * Returns specified application cache directory. Cache directory will be * created on SD card by defined path if card is mounted and app has * appropriate permission. Else - Android defines cache directory on * device's file system. * * @param cacheDir Cache directory path (e.g.: "AppCacheDir", * "AppDir/cache/images") * @return Cache {@link File directory} */ public static File getOwnCacheDirectory(Context context, String cacheDir, boolean preferExternal) { assert context != null; File appCacheDir = null; if (preferExternal && MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) && hasExternalStoragePermission(context)) { appCacheDir = new File(Environment.getExternalStorageDirectory(), cacheDir); } if (appCacheDir == null || (!appCacheDir.exists() && !appCacheDir.mkdirs())) { appCacheDir = context.getCacheDir(); } return appCacheDir; } @SuppressLint("NewApi") public static long getDiskSizeRemain(Context context) { File path = getIndividualImageCacheDirectory(context); StatFs stat = new StatFs(path.getPath()); long result; long block; if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) { result = stat.getAvailableBlocksLong(); block = stat.getBlockSizeLong(); } else { result = stat.getAvailableBlocks(); block = stat.getBlockSize(); } return result * block; } private static File getExternalCacheDir(Context context) { assert context != null; File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data"); File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache"); if (!appCacheDir.exists()) { if (!appCacheDir.mkdirs()) { return null; } try { new File(appCacheDir, ".nomedia").createNewFile(); } catch (IOException e) { } } return appCacheDir; } private static boolean hasExternalStoragePermission(Context context) { assert context != null; int perm = context.checkCallingOrSelfPermission(EXTERNAL_STORAGE_PERMISSION); return perm == PackageManager.PERMISSION_GRANTED; } public static void closeIO(Closeable... io) { for (Closeable close : io) { if (close != null) { try { close.close(); close = null; } catch (IOException e) { e.printStackTrace(); } } } } public static String getMimeType(String path) { String defaultContentType = "application/octet-stream"; if (TextUtils.isEmpty(path)) return defaultContentType; FileNameMap fileNameMap = URLConnection.getFileNameMap(); String contentTypeFor = null; try { contentTypeFor = fileNameMap.getContentTypeFor(path); } catch (Throwable t) { PrintStream stream = null; // try { // stream = LogUtils.getErrorPrintStream(); // if (stream != null) { // t.printStackTrace(stream); // } // contentTypeFor = getMimeType1(path); // } catch (Throwable e) { // e.printStackTrace(); // } finally { // closeIO(stream); // } } return TextUtils.isEmpty(contentTypeFor) ? defaultContentType : contentTypeFor; } /** * @return The MIME type for the given file. */ public static String getMimeType(File file) { String extension = getFileExtension(file.getName()); return getMimeTypeByExtension(extension); // if (extension.length() > 0) // return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.substring(1)); // return "application/octet-stream"; } /** * @return The MIME type for the given file. */ public static String getMimeType1(String path) { String extension = getFileExtension(path); return getMimeTypeByExtension(extension); // if (extension.length() > 0) // return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.substring(1)); // return "application/octet-stream"; } /** * @return The MIME type for the given file. */ private static String getMimeTypeByExtension(String extension) { if (extension.length() > 0) return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); return "application/octet-stream"; } public static String getDefaultSavePath() { File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "oMiniBox"); if (!file.exists()) { file.mkdirs(); } return file.getAbsolutePath(); } /* Checks if external storage is available for read and write */ public static boolean isExternalStorageWritable() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { return true; } return false; } /* Checks if external storage is available to at least read */ public static boolean isExternalStorageReadable() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { return true; } return false; } public static long getFolderSize(File file) { long size = 0; File[] fileList = file.listFiles(); for (int i = 0; i < fileList.length; i++) { if (fileList[i].isDirectory()) { size = size + getFolderSize(fileList[i]); } else { size = size + fileList[i].length(); } } return size; } public static void deleteFolderFile(String filePath, boolean deleteThisPath) throws IOException { if (!TextUtils.isEmpty(filePath)) { File file = new File(filePath); if (file.isDirectory()) {// 处理目录 File files[] = file.listFiles(); for (int i = 0; i < files.length; i++) { deleteFolderFile(files[i].getAbsolutePath(), true); } } if (deleteThisPath) { if (!file.isDirectory()) {// 如果是文件,删除 file.delete(); } else {// 目录 if (file.listFiles().length == 0) {// 目录下没有文件或者目录,删除 file.delete(); } } } } } } ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/LogUtils.java ================================================ /* * Copyright 2014 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package record.wilson.flutter.com.flutter_plugin_record.utils; import android.content.Context; import android.util.Log; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintStream; import java.util.Date; public class LogUtils { public static String LOG_PREFIX = ""; private static int LOG_PREFIX_LENGTH = LOG_PREFIX.length(); private static int MAX_LOG_TAG_LENGTH = 23; public static boolean LOGGING_ENABLED = true; public static String makeLogTag(String str) { if (str.length() > MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH) { return LOG_PREFIX + str.substring(0, MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH - 1); } return LOG_PREFIX + str; } /** * Don't use this when obfuscating class names! */ public static String makeLogTag(Class cls) { return makeLogTag(cls.getSimpleName()); } public static String getTAG() { return LOG_PREFIX; } public static void LOGD(String message) { if (LOGGING_ENABLED) { Log.d(getTag(), message != null ? message : "message is null"); } } public static void LOGD(final String tag, String message) { if (LOGGING_ENABLED) { if (Log.isLoggable(tag, Log.DEBUG)) { Log.d(tag, message); } } } public static void LOGDPrintProcess(final String tag, String message) { if (LOGGING_ENABLED) { if (Log.isLoggable(tag, Log.DEBUG)) { String msg = getLogMessage(message); Log.d(tag, msg); } } } public static void LOGD(final String tag, String message, Throwable cause) { if (LOGGING_ENABLED) { if (Log.isLoggable(tag, Log.DEBUG)) { Log.d(tag, message, cause); } } } public static void LOGV(String message) { if (LOGGING_ENABLED) { Log.v(getTag(), message != null ? message : "message is null"); } } public static void LOGV(final String tag, String message) { if (LOGGING_ENABLED) { if (Log.isLoggable(tag, Log.VERBOSE)) { Log.v(tag, message); } } } public static void LOGV(final String tag, String message, Throwable cause) { if (LOGGING_ENABLED) { if (Log.isLoggable(tag, Log.VERBOSE)) { Log.v(tag, message, cause); } } } public static void LOGI(String message) { if (LOGGING_ENABLED) { Log.i(getTag(), message != null ? message : "message is null"); } } public static void LOGI(final String tag, String message) { if (LOGGING_ENABLED) { Log.i(tag, message); } } public static void LOGI(final String tag, String message, Throwable cause) { if (LOGGING_ENABLED) { Log.i(tag, message, cause); } } public static void LOGW(String message) { if (LOGGING_ENABLED) { Log.w(getTag(), message != null ? message : "message is null"); } } public static void LOGW(final String tag, String message) { if (LOGGING_ENABLED) { Log.w(tag, message); } } public static void LOGW(final String tag, String message, Throwable cause) { if (LOGGING_ENABLED) { Log.w(tag, message, cause); } } public static void LOGE(String message) { if (LOGGING_ENABLED) { Log.e(getTag(), message != null ? message : "message is null"); } } public static void LOGE(final String tag, String message) { if (LOGGING_ENABLED) { Log.e(tag, message); } } private static final int MAX_CHAR_ONE_LINE = 82; public static void LOGEPrintProcess(final String tag, String message) { if (LOGGING_ENABLED) { String msg = getLogMessage(message); Log.e(tag, msg); } } private static final int LOG_LENGTH = 100; private static String getLogMessage(String msg) { StringBuilder sb = new StringBuilder(); String[] split = msg.split("\n"); addPrintHead(sb); sb.append("\n") .append(getFullFillTopLine(new Date().toString(), LOG_PREFIX)) .append("\n"); for (int i = 0; i < split.length; i++) { sb.append(getLine(split[i])).append("\n"); } sb.append(getFullFillBottomLine("pid : " + android.os.Process.myPid(), "tid : " + Thread.currentThread().getId())).append("\n"); addPrintBottom(sb); return sb.toString(); } private static void addPrintBottom(StringBuilder sb) { sb.append("└"); fillLine(sb, LOG_LENGTH, "-"); sb.append("┘"); } private static final int PADDING_LEFT = 4; private static String getLine(String s) { int remind = LOG_LENGTH - 4 - s.getBytes().length - PADDING_LEFT; StringBuilder temp = new StringBuilder(); temp.append("├┤"); for (int j = 0; j < PADDING_LEFT; j++) { temp.append(" "); } temp.append(s); fillLine(temp, remind, " "); temp.append("├┤"); return temp.toString(); } private static final int POSITION_TOP = 0; private static final int POSITION_MIDDLE = 1; private static final int POSITION_BOTTOM = 2; private static String getFullFillTopLine(String... s) { return getFullFillLine(POSITION_TOP, s); } private static String getFullFillBottomLine(String... s) { return getFullFillLine(POSITION_BOTTOM, s); } private static String getFullFillLine(int position, String... s) { int length = 0; for (int i = 0; i < s.length; i++) { int l = s[i].getBytes().length; length += l; } int remind = LOG_LENGTH - length - 4; int each = remind / (s.length + 1); int fix = each * (s.length + 1) - remind; StringBuilder temp = new StringBuilder(); switch (position) { case POSITION_TOP: temp.append("├┬"); break; case POSITION_MIDDLE: temp.append("├┤"); break; case POSITION_BOTTOM: temp.append("├┴"); break; } for (int j = 0; j < s.length; j++) { fillLine(temp, each, "-"); temp.append(s[j]); } fillLine(temp, each - fix, "-"); switch (position) { case POSITION_TOP: temp.append("┬┤"); break; case POSITION_MIDDLE: temp.append("├┤"); break; case POSITION_BOTTOM: temp.append("┴┤"); break; } return temp.toString(); } private static void fillLine(StringBuilder sb, int length, String filler) { int fill = length / filler.length(); for (int k = 0; k < fill; k++) { sb.append(filler); } } private static void addPrintHead(StringBuilder sb) { sb.append("┌"); fillLine(sb, LOG_LENGTH, "-"); sb.append("┐"); } public static void LOGE(final String tag, String message, Throwable cause) { if (LOGGING_ENABLED) { Log.e(tag, message, cause); } } public static void write2Log(Context context, String msg) { write2Log(context, msg, new StringBuilder(new Date().toString()).append("_log").toString()); } public static PrintStream getErrorPrintStream(Context context) throws FileNotFoundException { File logFile = new File(FileTool.getIndividualLogCacheDirectory(context), new StringBuilder(new Date().toString()).append("_log").toString() + ".txt"); return new PrintStream(new FileOutputStream(logFile)); } public static void write2Log(Context context, String msg, String name) { if (LOGGING_ENABLED) { File logFile = new File(FileTool.getCacheDirectory(context), name + ".txt"); BufferedWriter writer = null; try { if (!logFile.exists()) logFile.createNewFile(); writer = new BufferedWriter(new FileWriter(logFile, true)); writer.newLine(); writer.append(msg); } catch (IOException e) { e.printStackTrace(); } finally { FileTool.closeIO(writer); } } } private static String getTag() { StackTraceElement[] trace = new Throwable().fillInStackTrace() .getStackTrace(); String callingClass = ""; for (int i = 2; i < trace.length; i++) { Class clazz = trace[i].getClass(); if (!clazz.equals(Log.class)) { callingClass = trace[i].getClassName(); callingClass = callingClass.substring(callingClass .lastIndexOf('.') + 1); break; } } return callingClass; } private LogUtils() { } public static IntervalCounter getIntervalCounter() { return new IntervalCounter(); } public static class IntervalCounter { private long timeStamps; private IntervalCounter() { timeStamps = System.currentTimeMillis(); } public long getInterval() { long result = System.currentTimeMillis() - timeStamps; timeStamps = System.currentTimeMillis(); return result; } public long getTimeStamps() { timeStamps = System.currentTimeMillis(); return timeStamps; } public String getIntervalStr() { return " interval is " + getInterval(); } } public static String getCurCpuFreq() { String result = "N/A"; try { FileReader fr = new FileReader( "/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq"); BufferedReader br = new BufferedReader(fr); String text = br.readLine(); result = text.trim(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return result; } } ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/PlayState.java ================================================ package record.wilson.flutter.com.flutter_plugin_record.utils; public enum PlayState { prepare, start, pause, complete } ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/PlayUtilsPlus.java ================================================ package record.wilson.flutter.com.flutter_plugin_record.utils; import android.media.MediaPlayer; public class PlayUtilsPlus { PlayStateChangeListener playStateChangeListener; MediaPlayer player; public PlayUtilsPlus() { } public void setPlayStateChangeListener(PlayStateChangeListener listener) { this.playStateChangeListener = listener; // this.playStateChangeListener.onPlayStateChange(PlayState.prepare); } public void startPlaying(String filePath) { try { isPause=false; this.player = new MediaPlayer(); this.player.setDataSource(filePath); this.player.prepareAsync(); this.player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { public void onPrepared(MediaPlayer mp) { PlayUtilsPlus.this.player.start(); } }); if (this.playStateChangeListener != null) { // this.playStateChangeListener.onPlayStateChange(PlayState.start); } this.player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { public void onCompletion(MediaPlayer mp) { PlayUtilsPlus.this.stopPlaying(); } }); } catch (Exception var3) { var3.printStackTrace(); } } Boolean isPause = false; public boolean pausePlay() { try { if (this.player.isPlaying() && !isPause) { this.player.pause(); isPause = true; } else { this.player.start(); isPause = false; } } catch (Exception var2) { var2.printStackTrace(); } return isPause ; } public void stopPlaying() { try { if (this.player != null) { this.player.stop(); this.player.reset(); this.player=null; if (this.playStateChangeListener != null) { this.playStateChangeListener.onPlayStateChange(PlayState.complete); } } } catch (Exception var2) { var2.printStackTrace(); } } public boolean isPlaying() { try { return this.player != null && this.player.isPlaying(); } catch (Exception var2) { return false; } } public interface PlayStateChangeListener { void onPlayStateChange(PlayState playState); } } ================================================ FILE: android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/RecorderUtil.java ================================================ package record.wilson.flutter.com.flutter_plugin_record.utils; import android.os.Environment; import android.util.Log; import com.maple.recorder.recording.AudioChunk; import com.maple.recorder.recording.AudioRecordConfig; import com.maple.recorder.recording.MsRecorder; import com.maple.recorder.recording.PullTransport; import com.maple.recorder.recording.Recorder; import java.io.File; import java.util.Date; public class RecorderUtil { Recorder recorder; public static String rootPath = "/yun_ke_fu/flutter/wav_file/"; String voicePath; PlayUtilsPlus playUtils; RecordListener recordListener; PlayStateListener playStateListener; public RecorderUtil() { initVoice(); } public RecorderUtil(String path) { voicePath =path; } public void addPlayAmplitudeListener(RecordListener recordListener) { this.recordListener = recordListener; } public void addPlayStateListener(PlayStateListener playStateListener) { this.playStateListener = playStateListener; } private void initVoice() { initPath(); initVoicePath(); initRecorder(); } //初始化存储路径 private void initPath() { String ROOT = "";// /storage/emulated/0 if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { ROOT = Environment.getExternalStorageDirectory().getPath(); Log.e("voice", "系统方法:" + ROOT); } rootPath = ROOT + rootPath; File lrcFile = new File(rootPath); if (!lrcFile.exists()) { lrcFile.mkdirs(); } Log.e("voice", "初始存储路径" + rootPath); } private void initVoicePath() { String forDate = DateUtils.dateToString(new Date()); String name = "wav-" + forDate; voicePath = rootPath + name + ".wav"; Log.e("voice", "初始化语音路径" + voicePath); } private void initRecorder() { recorder = MsRecorder.wav( new File(voicePath), new AudioRecordConfig.Default(), new PullTransport.Default() .setOnAudioChunkPulledListener(new PullTransport.OnAudioChunkPulledListener() { @Override public void onAudioChunkPulled(AudioChunk audioChunk) { if (recordListener != null) { recordListener.onPlayAmplitude(audioChunk.maxAmplitude()); } } }) ); } public void startRecord() { if (recordListener != null) { recordListener.onVoicePathSuccess(voicePath); } recorder.stopRecording(); recorder.startRecording(); } public void stopRecord() { recorder.stopRecording(); } public void playVoice() { if (playUtils == null) { playUtils = new PlayUtilsPlus(); playUtils.setPlayStateChangeListener(new PlayUtilsPlus.PlayStateChangeListener() { @Override public void onPlayStateChange(PlayState playState) { playStateListener.playState(playState); } }); } if(playUtils.isPlaying()) { playUtils.stopPlaying(); } playUtils.startPlaying(voicePath); } public boolean pausePlay(){ LogUtils.LOGD("wilson","pausePlay"); boolean isPlaying = playUtils.pausePlay(); return isPlaying; } public void stopPlay(){ LogUtils.LOGD("wilson","stopPlay"); playUtils.stopPlaying(); } public interface RecordListener { void onPlayAmplitude(Double amplitude); void onVoicePathSuccess(String voicePath); } public interface PlayStateListener { void playState(PlayState playState); } } ================================================ FILE: example/.flutter-plugins-dependencies ================================================ {"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_plugin_record","path":"/Users/wilson/aochuang/FlutterDemo/flutter_plugin_record/","dependencies":[]},{"name":"path_provider","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/path_provider-1.6.24/","dependencies":[]},{"name":"shared_preferences","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.5.12+4/","dependencies":[]}],"android":[{"name":"flutter_plugin_record","path":"/Users/wilson/aochuang/FlutterDemo/flutter_plugin_record/","dependencies":[]},{"name":"path_provider","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/path_provider-1.6.24/","dependencies":[]},{"name":"shared_preferences","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.5.12+4/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/path_provider_macos-0.0.4+6/","dependencies":[]},{"name":"shared_preferences_macos","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_macos-0.0.1+11/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/path_provider_linux-0.0.1+2/","dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_linux-0.0.2+4/","dependencies":["path_provider_linux"]}],"windows":[{"name":"path_provider_windows","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/path_provider_windows-0.0.4+3/","dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_windows-0.0.1+3/","dependencies":["path_provider_windows"]}],"web":[{"name":"shared_preferences_web","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_web-0.1.2+7/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_plugin_record","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux","path_provider_windows"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_linux","shared_preferences_macos","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_macos","dependencies":[]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2021-03-24 11:27:57.607425","version":"2.0.3"} ================================================ FILE: example/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ .flutter-plugins .packages .pub-cache/ .pub/ /build/ # Android related **/android/**/gradle-wrapper.jar **/android/.gradle **/android/captures/ **/android/gradlew **/android/gradlew.bat **/android/local.properties **/android/**/GeneratedPluginRegistrant.java # iOS/XCode related **/ios/**/*.mode1v3 **/ios/**/*.mode2v3 **/ios/**/*.moved-aside **/ios/**/*.pbxuser **/ios/**/*.perspectivev3 **/ios/**/*sync/ **/ios/**/.sconsign.dblite **/ios/**/.tags* **/ios/**/.vagrant/ **/ios/**/DerivedData/ **/ios/**/Icon? **/ios/**/Pods/ **/ios/**/.symlinks/ **/ios/**/profile **/ios/**/xcuserdata **/ios/.generated/ **/ios/Flutter/App.framework **/ios/Flutter/Flutter.framework **/ios/Flutter/Generated.xcconfig **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ **/ios/Flutter/flutter_export_environment.sh **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages ================================================ FILE: example/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: 68587a0916366e9512a78df22c44163d041dd5f3 channel: stable project_type: app ================================================ FILE: example/README.md ================================================ # flutter_plugin_record_example ### 使用 ### 1, 初始化录制 可以在页面初始化的时候进行初始化比如: 在initState方法中进行初始化 //实例化对象 FlutterPluginRecord recordPlugin = new FlutterPluginRecord(); // 初始化 recordPlugin.init() ### 2, 开始录制 recordPlugin.start() ### 3, 停止录制 recordPlugin.stop() ### 4, 播放 暂停,停止播放 #### 1,播放 recordPlugin.play() #### 2, 暂停和继续播放 recordPlugin.pausePlay(); #### 3, 停止播放 recordPlugin.stopPlay(); ### 3, 释放资源 可以在页面退出的时候进行资源释放 比如在 dispose方法中调用如下代码 recordPlugin.dispose() ### 4,回调监听 1,初始化回调监听 ///初始化方法的监听 recordPlugin.responseFromInit.listen((data) { if (data) { print("初始化成功"); } else { print("初始化失败"); } }); 2,开始录制停止录制监听 /// 开始录制或结束录制的监听 recordPlugin.response.listen((data) { if (data.msg == "onStop") { ///结束录制时会返回录制文件的地址方便上传服务器 print("onStop " + data.path); } else if (data.msg == "onStart") { print("onStart --"); } }); 3,录制声音大小回调监听 ///录制过程监听录制的声音的大小 方便做语音动画显示图片的样式 recordPlugin.responseFromAmplitude.listen((data) { var voiceData = double.parse(data.msg); var tempVoice = ""; if (voiceData > 0 && voiceData < 0.1) { tempVoice = "images/voice_volume_2.png"; } else if (voiceData > 0.2 && voiceData < 0.3) { tempVoice = "images/voice_volume_3.png"; } else if (voiceData > 0.3 && voiceData < 0.4) { tempVoice = "images/voice_volume_4.png"; } else if (voiceData > 0.4 && voiceData < 0.5) { tempVoice = "images/voice_volume_5.png"; } else if (voiceData > 0.5 && voiceData < 0.6) { tempVoice = "images/voice_volume_6.png"; } else if (voiceData > 0.6 && voiceData < 0.7) { tempVoice = "images/voice_volume_7.png"; } else if (voiceData > 0.7 && voiceData < 1) { tempVoice = "images/voice_volume_7.png"; } setState(() { voiceIco = tempVoice; if(overlayEntry!=null){ overlayEntry.markNeedsBuild(); } }); print("振幅大小 " + voiceData.toString() + " " + voiceIco); }); ## 2,录制组件的使用 ### 1,在使用的页面进行导入package import 'package:flutter_plugin_record/index.dart'; ### 2,在使用的地方引入VoiceWidget组件 new VoiceWidget(), ## TODO * [x] 实现发送语音时间按下抬起时间很短提示 * [x] 优化代码 * [x] 实现录制完成文件路径回调功能,方面使用者可以把录音文件上传服务器 ## 关注公众号获取更多内容 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190926100941125.jpg) ================================================ FILE: example/android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties ================================================ FILE: example/android/app/build.gradle ================================================ def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion 29 sourceSets { main.java.srcDirs += 'src/main/kotlin' } lintOptions { disable 'InvalidPackage' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "record.wilson.flutter.com.flutter_plugin_record_example" minSdkVersion 19 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug } } } flutter { source '../..' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' } ================================================ FILE: example/android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: example/android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: example/android/app/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record_example/MainActivity.kt ================================================ package record.wilson.flutter.com.flutter_plugin_record_example import io.flutter.embedding.android.FlutterActivity class MainActivity: FlutterActivity() { } ================================================ FILE: example/android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: example/android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: example/android/app/src/main/res/xml/network_security_config.xml ================================================ ================================================ FILE: example/android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: example/android/build.gradle ================================================ buildscript { // ext.kotlin_version = '1.2.71' ext.kotlin_version = '1.3.50' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() jcenter() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: example/android/gradle/wrapper/gradle-wrapper.properties ================================================ #Wed Dec 18 09:02:45 CST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip ================================================ FILE: example/android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.enableR8=true android.useAndroidX=true android.enableJetifier=true ================================================ FILE: example/android/res/values/strings_en.arb ================================================ {} ================================================ FILE: example/android/settings.gradle ================================================ include ':app' def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() def plugins = new Properties() def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') if (pluginsFile.exists()) { pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } } plugins.each { name, path -> def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() include ":$name" project(":$name").projectDir = pluginDirectory } ================================================ FILE: example/ios/Flutter/.last_build_id ================================================ dcb78416cfbfe60288e8c774d67c139c ================================================ FILE: example/ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 8.0 ================================================ FILE: example/ios/Flutter/Debug.xcconfig ================================================ #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: example/ios/Flutter/Flutter.podspec ================================================ # # NOTE: This podspec is NOT to be published. It is only used as a local source! # This is a generated file; do not edit or check into version control. # Pod::Spec.new do |s| s.name = 'Flutter' s.version = '1.0.0' s.summary = 'High-performance, high-fidelity mobile apps.' s.homepage = 'https://flutter.io' s.license = { :type => 'MIT' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } s.ios.deployment_target = '8.0' # Framework linking is handled by Flutter tooling, not CocoaPods. # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. s.vendored_frameworks = 'path/to/nothing' end ================================================ FILE: example/ios/Flutter/Release.xcconfig ================================================ #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: example/ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '9.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: example/ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. ================================================ FILE: example/ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: example/ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: example/ios/Runner/Info.plist ================================================ NSAppTransportSecurity NSAllowsArbitraryLoads CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName flutter_plugin_record_example CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS NSMicrophoneUsageDescription 打开话筒 UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance ================================================ FILE: example/ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: example/ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 481940F04663AD0B6D9C404B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6140FFF951F9E4BD7AB5108D /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 1935C927EF54753EFE93F347 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 26B885D5752B3F53CA047B5E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 6140FFF951F9E4BD7AB5108D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F345A59A40866AC079FB2659 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 481940F04663AD0B6D9C404B /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 61F2D205163EA3AEA9F3A435 /* Frameworks */ = { isa = PBXGroup; children = ( 6140FFF951F9E4BD7AB5108D /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; 9475E7547E3F8A95EB266079 /* Pods */ = { isa = PBXGroup; children = ( 26B885D5752B3F53CA047B5E /* Pods-Runner.debug.xcconfig */, 1935C927EF54753EFE93F347 /* Pods-Runner.release.xcconfig */, F345A59A40866AC079FB2659 /* Pods-Runner.profile.xcconfig */, ); name = Pods; path = Pods; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 9475E7547E3F8A95EB266079 /* Pods */, 61F2D205163EA3AEA9F3A435 /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 504AAE8408988510F90F1B85 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, A16AE44BD745529DCD8AAD30 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1020; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 504AAE8408988510F90F1B85 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; A16AE44BD745529DCD8AAD30 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: example/ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: example/lib/generated/i18n.dart ================================================ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; // ignore_for_file: non_constant_identifier_names // ignore_for_file: camel_case_types // ignore_for_file: prefer_single_quotes // This file is automatically generated. DO NOT EDIT, all your changes would be lost. class S implements WidgetsLocalizations { const S(); static S current; static const GeneratedLocalizationsDelegate delegate = GeneratedLocalizationsDelegate(); static S of(BuildContext context) => Localizations.of(context, S); @override TextDirection get textDirection => TextDirection.ltr; } class $en extends S { const $en(); } class GeneratedLocalizationsDelegate extends LocalizationsDelegate { const GeneratedLocalizationsDelegate(); List get supportedLocales { return const [ Locale("en", ""), ]; } LocaleListResolutionCallback listResolution({Locale fallback, bool withCountry = true}) { return (List locales, Iterable supported) { if (locales == null || locales.isEmpty) { return fallback ?? supported.first; } else { return _resolve(locales.first, fallback, supported, withCountry); } }; } LocaleResolutionCallback resolution({Locale fallback, bool withCountry = true}) { return (Locale locale, Iterable supported) { return _resolve(locale, fallback, supported, withCountry); }; } @override Future load(Locale locale) { final String lang = getLang(locale); if (lang != null) { switch (lang) { case "en": S.current = const $en(); return SynchronousFuture(S.current); default: // NO-OP. } } S.current = const S(); return SynchronousFuture(S.current); } @override bool isSupported(Locale locale) => _isSupported(locale, true); @override bool shouldReload(GeneratedLocalizationsDelegate old) => false; /// /// Internal method to resolve a locale from a list of locales. /// Locale _resolve(Locale locale, Locale fallback, Iterable supported, bool withCountry) { if (locale == null || !_isSupported(locale, withCountry)) { return fallback ?? supported.first; } final Locale languageLocale = Locale(locale.languageCode, ""); if (supported.contains(locale)) { return locale; } else if (supported.contains(languageLocale)) { return languageLocale; } else { final Locale fallbackLocale = fallback ?? supported.first; return fallbackLocale; } } /// /// Returns true if the specified locale is supported, false otherwise. /// bool _isSupported(Locale locale, bool withCountry) { if (locale != null) { for (Locale supportedLocale in supportedLocales) { // Language must always match both locales. if (supportedLocale.languageCode != locale.languageCode) { continue; } // If country code matches, return this locale. if (supportedLocale.countryCode == locale.countryCode) { return true; } // If no country requirement is requested, check if this locale has no country. if (true != withCountry && (supportedLocale.countryCode == null || supportedLocale.countryCode.isEmpty)) { return true; } } } return false; } } String getLang(Locale l) => l == null ? null : l.countryCode != null && l.countryCode.isEmpty ? l.languageCode : l.toString(); ================================================ FILE: example/lib/main.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_plugin_record_example/path_provider_screen.dart'; import 'package:flutter_plugin_record_example/record_mp3_screen.dart'; import 'package:flutter_plugin_record_example/record_screen.dart'; import 'package:flutter_plugin_record_example/wechat_record_screen.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home: new MyHomePage(title: 'Flutter Demo Home Page'), routes: { "RecordScreen": (BuildContext context) => new RecordScreen(), "RecordMp3Screen": (BuildContext context) => new RecordMp3Screen(), "WeChatRecordScreen": (BuildContext context) => new WeChatRecordScreen(), "PathProviderScreen": (BuildContext context) => new PathProviderScreen(), }, ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => new _MyHomePageState(); } class _MyHomePageState extends State { @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("flutter版微信语音录制实现"), ), body: new Center( child: new Column( mainAxisSize: MainAxisSize.min, children: [ new FlatButton( onPressed: () { Navigator.pushNamed(context, "RecordScreen"); }, child: new Text("进入语音录制界面")), new FlatButton( onPressed: () { Navigator.pushNamed(context, "RecordMp3Screen"); }, child: new Text("进入录制mp3模式")), new FlatButton( onPressed: () { Navigator.pushNamed(context, "WeChatRecordScreen"); }, child: new Text("进入仿微信录制界面")), new FlatButton( onPressed: () { Navigator.pushNamed(context, "PathProviderScreen"); }, child: new Text("进入文件路径获取界面")), ], ), ), ); } } ================================================ FILE: example/lib/path_provider_screen.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; class PathProviderScreen extends StatefulWidget { PathProviderScreen({Key key, this.title}) : super(key: key); final String title; @override _PathProviderScreenState createState() => _PathProviderScreenState(); } class _PathProviderScreenState extends State { Future _tempDirectory; Future _appSupportDirectory; Future _appLibraryDirectory; Future _appDocumentsDirectory; Future _externalDocumentsDirectory; Future> _externalStorageDirectories; Future> _externalCacheDirectories; void _requestTempDirectory() { setState(() { _tempDirectory = getTemporaryDirectory(); }); } Widget _buildDirectory( BuildContext context, AsyncSnapshot snapshot) { Text text = const Text(''); if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError) { text = Text('Error: ${snapshot.error}'); } else if (snapshot.hasData) { text = Text('path: ${snapshot.data.path}'); } else { text = const Text('path unavailable'); } } return Padding(padding: const EdgeInsets.all(16.0), child: text); } Widget _buildDirectories( BuildContext context, AsyncSnapshot> snapshot) { Text text = const Text(''); if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError) { text = Text('Error: ${snapshot.error}'); } else if (snapshot.hasData) { final String combined = snapshot.data.map((Directory d) => d.path).join(', '); text = Text('paths: $combined'); } else { text = const Text('path unavailable'); } } return Padding(padding: const EdgeInsets.all(16.0), child: text); } void _requestAppDocumentsDirectory() { setState(() { _appDocumentsDirectory = getApplicationDocumentsDirectory(); }); } void _requestAppSupportDirectory() { setState(() { _appSupportDirectory = getApplicationSupportDirectory(); }); } void _requestAppLibraryDirectory() { setState(() { _appLibraryDirectory = getLibraryDirectory(); }); } void _requestExternalStorageDirectory() { setState(() { _externalDocumentsDirectory = getExternalStorageDirectory(); }); } void _requestExternalStorageDirectories(StorageDirectory type) { setState(() { _externalStorageDirectories = getExternalStorageDirectories(type: type); }); } void _requestExternalCacheDirectories() { setState(() { _externalCacheDirectories = getExternalCacheDirectories(); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("获取文件路径界面"), ), body: Center( child: ListView( children: [ Padding( padding: const EdgeInsets.all(16.0), child: RaisedButton( child: const Text('Get Temporary Directory'), onPressed: _requestTempDirectory, ), ), FutureBuilder( future: _tempDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), child: RaisedButton( child: const Text('Get Application Documents Directory'), onPressed: _requestAppDocumentsDirectory, ), ), FutureBuilder( future: _appDocumentsDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), child: RaisedButton( child: const Text('Get Application Support Directory'), onPressed: _requestAppSupportDirectory, ), ), FutureBuilder( future: _appSupportDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), child: RaisedButton( child: const Text('Get Application Library Directory'), onPressed: _requestAppLibraryDirectory, ), ), FutureBuilder( future: _appLibraryDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), child: RaisedButton( child: Text( '${Platform.isIOS ? "External directories are unavailable " "on iOS" : "Get External Storage Directory"}'), onPressed: Platform.isIOS ? null : _requestExternalStorageDirectory, ), ), FutureBuilder( future: _externalDocumentsDirectory, builder: _buildDirectory), Column(children: [ Padding( padding: const EdgeInsets.all(16.0), child: RaisedButton( child: Text( '${Platform.isIOS ? "External directories are unavailable " "on iOS" : "Get External Storage Directories"}'), onPressed: Platform.isIOS ? null : () { _requestExternalStorageDirectories( StorageDirectory.music, ); }, ), ), ]), FutureBuilder>( future: _externalStorageDirectories, builder: _buildDirectories), Column(children: [ Padding( padding: const EdgeInsets.all(16.0), child: RaisedButton( child: Text( '${Platform.isIOS ? "External directories are unavailable " "on iOS" : "Get External Cache Directories"}'), onPressed: Platform.isIOS ? null : _requestExternalCacheDirectories, ), ), ]), FutureBuilder>( future: _externalCacheDirectories, builder: _buildDirectories), ], ), ), ); } } ================================================ FILE: example/lib/record_mp3_screen.dart ================================================ import 'dart:io'; import 'package:flustars/flustars.dart'; import 'package:flutter/material.dart'; import 'package:flutter_plugin_record/flutter_plugin_record.dart'; import 'package:path_provider/path_provider.dart'; class RecordMp3Screen extends StatefulWidget { @override _RecordMp3ScreenState createState() => _RecordMp3ScreenState(); } class _RecordMp3ScreenState extends State { FlutterPluginRecord recordPlugin = new FlutterPluginRecord(); String filePath = ""; @override void initState() { super.initState(); ///初始化方法的监听 recordPlugin.responseFromInit.listen((data) { if (data) { print("初始化成功"); } else { print("初始化失败"); } }); /// 开始录制或结束录制的监听 recordPlugin.response.listen((data) { if (data.msg == "onStop") { ///结束录制时会返回录制文件的地址方便上传服务器 print("onStop 文件路径" + data.path); filePath = data.path; print("onStop 时长 " + data.audioTimeLength.toString()); } else if (data.msg == "onStart") { print("onStart --"); } else { print("--" + data.msg); } }); ///录制过程监听录制的声音的大小 方便做语音动画显示图片的样式 recordPlugin.responseFromAmplitude.listen((data) { var voiceData = double.parse(data.msg); print("振幅大小 " + voiceData.toString()); }); recordPlugin.responsePlayStateController.listen((data) { print("播放路径 " + data.playPath); print("播放状态 " + data.playState); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('录制mp3'), ), body: Center( child: Column( children: [ FlatButton( child: Text("初始化录制mp3"), onPressed: () { _initRecordMp3(); }, ), FlatButton( child: Text("开始录制"), onPressed: () { start(); }, ), FlatButton( child: Text("根据路径录制mp3文件"), onPressed: () { _requestAppDocumentsDirectory(); }, ), FlatButton( child: Text("停止录制"), onPressed: () { stop(); }, ), FlatButton( child: Text("播放"), onPressed: () { play(); }, ), FlatButton( child: Text("播放本地指定路径录音文件"), onPressed: () { playByPath(filePath,"file"); }, ), FlatButton( child: Text("播放网络mp3文件"), onPressed: () { playByPath("https://test-1259809289.cos.ap-nanjing.myqcloud.com/temp.mp3","url"); }, ), FlatButton( child: Text("暂停|继续播放"), onPressed: () { pause(); }, ), FlatButton( child: Text("停止播放"), onPressed: () { stopPlay(); }, ), ], ), ), ); } void _requestAppDocumentsDirectory() { // if(Platform.isIOS){ // //ios相关代码 // setState(() { // getApplicationDocumentsDirectory().then((value) { // String nowDataTimeStr = DateUtil.getNowDateMs().toString(); // String wavPath = value.path + "/" + nowDataTimeStr + ".wav"; // startByWavPath(wavPath); // }); // }); // }else if(Platform.isAndroid){ // //android相关代码 // } setState(() { getApplicationDocumentsDirectory().then((value) { String nowDataTimeStr = DateUtil.getNowDateMs().toString(); // TODO 注意IOS 传递的Mp3路径一定是以 .MP3 结尾 String wavPath =""; if (Platform.isIOS) { wavPath = value.path + "/" + nowDataTimeStr+".MP3"; }else{ wavPath = value.path + "/" + nowDataTimeStr; } startByWavPath(wavPath); }); }); } ///初始化语音录制的方法 void _initRecordMp3() async { recordPlugin.initRecordMp3(); } ///开始语音录制的方法 void start() async { recordPlugin.start(); } ///根据传递的路径进行语音录制 void startByWavPath(String wavPath) async { recordPlugin.startByWavPath(wavPath); } ///停止语音录制的方法 void stop() { recordPlugin.stop(); } ///播放语音的方法 void play() { recordPlugin.play(); } ///播放指定路径录音文件 url为iOS播放网络语音,file为播放本地语音文件 void playByPath(String path,String type) { recordPlugin.playByPath(path,type); } ///暂停|继续播放 void pause() { recordPlugin.pausePlay(); } @override void dispose() { /// 当界面退出的时候是释放录音资源 recordPlugin.dispose(); super.dispose(); } void stopPlay() { recordPlugin.stopPlay(); } } ================================================ FILE: example/lib/record_screen.dart ================================================ import 'dart:io'; import 'package:flustars/flustars.dart'; import 'package:flutter/material.dart'; import 'package:flutter_plugin_record/flutter_plugin_record.dart'; import 'package:path_provider/path_provider.dart'; class RecordScreen extends StatefulWidget { @override _RecordScreenState createState() => _RecordScreenState(); } class _RecordScreenState extends State { FlutterPluginRecord recordPlugin = new FlutterPluginRecord(); String filePath = ""; @override void initState() { super.initState(); ///初始化方法的监听 recordPlugin.responseFromInit.listen((data) { if (data) { print("初始化成功"); } else { print("初始化失败"); } }); /// 开始录制或结束录制的监听 recordPlugin.response.listen((data) { if (data.msg == "onStop") { ///结束录制时会返回录制文件的地址方便上传服务器 print("onStop 文件路径" + data.path); filePath = data.path; print("onStop 时长 " + data.audioTimeLength.toString()); } else if (data.msg == "onStart") { print("onStart --"); } else { print("--" + data.msg); } }); ///录制过程监听录制的声音的大小 方便做语音动画显示图片的样式 recordPlugin.responseFromAmplitude.listen((data) { var voiceData = double.parse(data.msg); print("振幅大小 " + voiceData.toString()); }); recordPlugin.responsePlayStateController.listen((data) { print("播放路径 " + data.playPath); print("播放状态 " + data.playState); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('录制wav'), ), body: Center( child: Column( children: [ FlatButton( child: Text("初始化"), onPressed: () { _init(); }, ), FlatButton( child: Text("开始录制"), onPressed: () { start(); }, ), FlatButton( child: Text("根据路径录制wav文件"), onPressed: () { _requestAppDocumentsDirectory(); }, ), FlatButton( child: Text("停止录制"), onPressed: () { stop(); }, ), FlatButton( child: Text("播放"), onPressed: () { play(); }, ), FlatButton( child: Text("播放本地指定路径录音文件"), onPressed: () { playByPath(filePath,"file"); }, ), FlatButton( child: Text("播放网络wav文件"), onPressed: () { playByPath("https://test-1259809289.cos.ap-nanjing.myqcloud.com/test.wav","url"); }, ), FlatButton( child: Text("暂停|继续播放"), onPressed: () { pause(); }, ), FlatButton( child: Text("停止播放"), onPressed: () { stopPlay(); }, ), ], ), ), ); } void _requestAppDocumentsDirectory() { // if(Platform.isIOS){ // //ios相关代码 // setState(() { // getApplicationDocumentsDirectory().then((value) { // String nowDataTimeStr = DateUtil.getNowDateMs().toString(); // String wavPath = value.path + "/" + nowDataTimeStr + ".wav"; // startByWavPath(wavPath); // }); // }); // }else if(Platform.isAndroid){ // //android相关代码 // } setState(() { getApplicationDocumentsDirectory().then((value) { String nowDataTimeStr = DateUtil.getNowDateMs().toString(); String wavPath = value.path + "/" + nowDataTimeStr + ".wav"; print(wavPath); startByWavPath(wavPath); }); }); } ///初始化语音录制的方法 void _init() async { recordPlugin.init(); } ///开始语音录制的方法 void start() async { recordPlugin.start(); } ///根据传递的路径进行语音录制 void startByWavPath(String wavPath) async { recordPlugin.startByWavPath(wavPath); } ///停止语音录制的方法 void stop() { recordPlugin.stop(); } ///播放语音的方法 void play() { recordPlugin.play(); } ///播放指定路径录音文件 url为iOS播放网络语音,file为播放本地语音文件 void playByPath(String path,String type) { recordPlugin.playByPath(path,type); } ///暂停|继续播放 void pause() { recordPlugin.pausePlay(); } @override void dispose() { /// 当界面退出的时候是释放录音资源 recordPlugin.dispose(); super.dispose(); } void stopPlay() { recordPlugin.stopPlay(); } } ================================================ FILE: example/lib/wechat_record_screen.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_plugin_record/index.dart'; class WeChatRecordScreen extends StatefulWidget { @override _WeChatRecordScreenState createState() => _WeChatRecordScreenState(); } class _WeChatRecordScreenState extends State { String toastShow = "悬浮框"; OverlayEntry overlayEntry; showView(BuildContext context) { if (overlayEntry == null) { overlayEntry = new OverlayEntry(builder: (content) { return Positioned( top: MediaQuery.of(context).size.height * 0.5 - 80, left: MediaQuery.of(context).size.width * 0.5 - 80, child: Material( child: Center( child: Opacity( opacity: 0.8, child: Container( width: 100, height: 100, decoration: BoxDecoration( color: Color(0xff77797A), borderRadius: BorderRadius.all(Radius.circular(20.0)), ), child: Column( children: [ Container( // padding: EdgeInsets.only(right: 20, left: 20, top: 0), child: Text( toastShow, style: TextStyle( fontStyle: FontStyle.normal, color: Colors.white, fontSize: 14, ), ), ) ], ), ), ), ), ), ); }); Overlay.of(context).insert(overlayEntry); } } startRecord() { print("开始录制"); } stopRecord(String path, double audioTimeLength) { print("结束束录制"); print("音频文件位置" + path); print("音频录制时长" + audioTimeLength.toString()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("仿微信发送语音"), ), body: Container( child: Column( children: [ new FlatButton( onPressed: () { showView(context); }, child: new Text("悬浮组件")), new FlatButton( onPressed: () { if (overlayEntry != null) { overlayEntry.remove(); overlayEntry = null; } }, child: new Text("隐藏悬浮组件")), new FlatButton( onPressed: () { setState(() { toastShow = "111"; if (overlayEntry != null) { overlayEntry.markNeedsBuild(); } }); }, child: new Text("悬浮窗状态更新")), new VoiceWidget( startRecord: startRecord, stopRecord: stopRecord, // 加入定制化Container的相关属性 height: 40.0, ), ], ), ), ); } } ================================================ FILE: example/pubspec.yaml ================================================ name: flutter_plugin_record_example description: The flutter voice recording plug-in,provides the recording animation and the recording successfully returns to the recording file path # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 1.0.0+1 environment: sdk: ">=2.7.0 <3.0.0" dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.3 path_provider: ^1.6.8 flustars: ^0.3.2 dev_dependencies: flutter_test: sdk: flutter flutter_plugin_record: path: ../ # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: example/test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility that Flutter provides. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_plugin_record_example/main.dart'; void main() { testWidgets('Verify Platform version', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(MyApp()); // Verify that platform version is retrieved. expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data.startsWith('Running on:'), ), findsOneWidget, ); }); } ================================================ FILE: flutter_plugin_record.iml ================================================ ================================================ FILE: ios/.gitignore ================================================ .idea/ .vagrant/ .sconsign.dblite .svn/ .DS_Store *.swp profile DerivedData/ build/ GeneratedPluginRegistrant.h GeneratedPluginRegistrant.m .generated/ *.pbxuser *.mode1v3 *.mode2v3 *.perspectivev3 !default.pbxuser !default.mode1v3 !default.mode2v3 !default.perspectivev3 xcuserdata *.moved-aside *.pyc *sync/ Icon? .tags* /Flutter/Generated.xcconfig /Flutter/flutter_export_environment.sh ================================================ FILE: ios/Assets/.gitkeep ================================================ ================================================ FILE: ios/Classes/DPAudioPlayer.h ================================================ #import typedef void(^PlayCompleteBlock)(void); typedef void(^StartPlayingBlock)(BOOL isPlaying); @interface DPAudioPlayer : NSObject /** 播放完成回调 */ @property (nonatomic, copy) PlayCompleteBlock playComplete; /** 开始播放回调 */ @property (nonatomic, copy) StartPlayingBlock startPlaying; + (DPAudioPlayer *)sharedInstance; /** 播放data格式录音 @param data 录音data */ - (void)startPlayWithData:(NSData *)data; /** 停止播放 */ - (void)stopPlaying; /// 暂停播放 - (bool)pausePlaying; @end ================================================ FILE: ios/Classes/DPAudioPlayer.m ================================================ #import "DPAudioPlayer.h" #import #import @interface DPAudioPlayer () { BOOL isPlaying; } @property (nonatomic, strong) AVAudioPlayer *audioPlayer; @end @implementation DPAudioPlayer static DPAudioPlayer *playerManager = nil; + (DPAudioPlayer *)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken,^{ playerManager = [[DPAudioPlayer alloc] init]; }); return playerManager; } - (instancetype)init { if (self) { //创建缓存录音文件到Tmp NSString *wavPlayerFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WAVtemporaryPlayer.wav"]; NSString *amrPlayerFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"AMRtemporaryPlayer.amr"]; if (![[NSFileManager defaultManager]fileExistsAtPath:wavPlayerFilePath]) { [[NSData data] writeToFile:wavPlayerFilePath atomically:YES]; } if (![[NSFileManager defaultManager]fileExistsAtPath:amrPlayerFilePath]) { [[NSData data] writeToFile:amrPlayerFilePath atomically:YES]; } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(proximityStateDidChange) name:UIDeviceProximityStateDidChangeNotification object:nil]; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; [[AVAudioSession sharedInstance] setActive:YES error:nil]; } return self; } - (void)startPlayWithData:(NSData *)data { // if (isPlaying) return; //打开红外传感器 [[UIDevice currentDevice] setProximityMonitoringEnabled:YES]; AVAudioSession *session = [AVAudioSession sharedInstance]; [session setActive:true error:nil]; [session setCategory:AVAudioSessionCategoryPlayback error:nil]; // //默认情况下扬声器播放 // AVAudioSessionPortOverride portOverride = AVAudioSessionPortOverrideNone; // [[AVAudioSession sharedInstance] overrideOutputAudioPort:portOverride error:nil]; //self.audioPlayer = [[AVAudioPlayer alloc]initWithData:[self conversionAMRDataToWAVData:data] error:nil]; if (isPlaying){ [self.audioPlayer stop]; self.audioPlayer = nil; isPlaying = NO; } self.audioPlayer = [[AVAudioPlayer alloc]initWithData:data error:nil]; self.audioPlayer.meteringEnabled = YES; self.audioPlayer.delegate = self; self.audioPlayer.volume = 1.0; self.audioPlayer.numberOfLoops = 0; [self.audioPlayer prepareToPlay]; [self.audioPlayer play]; if ([self.audioPlayer isPlaying]) { isPlaying = YES; if (self.startPlaying) { self.startPlaying(YES); } } else { isPlaying = NO; if (self.startPlaying) { self.startPlaying(NO); } } } //暂停播放 - (bool)pausePlaying { if (isPlaying){ //关闭红外传感器 [[UIDevice currentDevice] setProximityMonitoringEnabled:NO]; [self.audioPlayer pause]; isPlaying = NO; }else{ [self.audioPlayer play]; isPlaying = YES; } return isPlaying; } - (void)stopPlaying { if (!isPlaying) return; //关闭红外传感器 [[UIDevice currentDevice] setProximityMonitoringEnabled:NO]; [self.audioPlayer stop]; self.audioPlayer = nil; isPlaying = NO; } - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag { if (flag) { [self stopPlaying]; if (self.playComplete) { self.playComplete(); } } } - (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer*)player error:(NSError *)error{ //解码错误执行的动作 NSLog(@""); } //- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player //{ // isPlaying = NO; // [player stop]; //} ////转换amr文件类型data为wav文件类型data //- (NSData *)conversionAMRDataToWAVData:(NSData *)amrData //{ // // NSString *wavPlayerFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WAVtemporaryPlayer.wav"]; // NSString *amrPlayerFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"AMRtemporaryPlayer.amr"]; // // //amr的data写入文件 // [amrData writeToFile:amrPlayerFilePath atomically:YES]; // //将AMR文件转码成WAVE文件 // amr_file_to_wave_file([amrPlayerFilePath cStringUsingEncoding:NSUTF8StringEncoding], // [wavPlayerFilePath cStringUsingEncoding:NSUTF8StringEncoding]); // // //得到转码后wav的data // return [NSData dataWithContentsOfFile:wavPlayerFilePath]; //} - (void)proximityStateDidChange { if ([UIDevice currentDevice].proximityState) { NSLog(@"有物品靠近"); [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; } else { NSLog(@"有物品离开"); [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; } } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceProximityStateDidChangeNotification object:nil]; [[UIDevice currentDevice] setProximityMonitoringEnabled:NO]; } @end ================================================ FILE: ios/Classes/DPAudioRecorder.h ================================================ #import typedef void(^AudioRecorderFinishRecordingBlock)(NSData *data, NSTimeInterval audioTimeLength,NSString *path); typedef void(^AudioStartRecordingBlock)(BOOL isRecording); typedef void(^AudioRecordingFailBlock)(NSString *reason); typedef void(^AudioSpeakPowerBlock)(float power); /// 录制语音 @interface DPAudioRecorder : NSObject /// 录制完成的回调 @property (nonatomic, copy) AudioRecorderFinishRecordingBlock audioRecorderFinishRecording; /// 开始录制回调 @property (nonatomic, copy) AudioStartRecordingBlock audioStartRecording; /// 录制失败回调 @property (nonatomic, copy) AudioRecordingFailBlock audioRecordingFail; /// 音频值测量回调 @property (nonatomic, copy) AudioSpeakPowerBlock audioSpeakPower; + (DPAudioRecorder *)sharedInstance; /// 传递录制文件路径 - (void)initByWavPath:(NSString*) wavPath; - (void)initByMp3; /// 开始录音方法 - (void)startRecording; /// 停止录音方法 - (void)stopRecording; @end ================================================ FILE: ios/Classes/DPAudioRecorder.m ================================================ #import "DPAudioRecorder.h" #import "DPAudioPlayer.h" #import #import #import "JX_GCDTimerManager.h" #define MAX_RECORDER_TIME 2100 //最大录制时间 #define MIN_RECORDER_TIME 1 // 最小录制时间 #define TimerName @"audioTimer_999" //定义音频枚举类型 typedef NS_ENUM(NSUInteger, CSVoiceType) { CSVoiceTypeWav, CSVoiceTypeAmr }; static const CSVoiceType preferredVoiceType = CSVoiceTypeWav; @interface DPAudioRecorder () { BOOL isRecording; dispatch_source_t timer; NSTimeInterval __block audioTimeLength; //录音时长 } @property (nonatomic, strong) AVAudioRecorder *audioRecorder; @property (nonatomic, strong) NSString *originWaveFilePath; @end @implementation DPAudioRecorder static DPAudioRecorder *recorderManager = nil; + (DPAudioRecorder *)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken,^{ recorderManager = [[DPAudioRecorder alloc] init]; }); return recorderManager; } /// 默认构造方法 - (instancetype)init { if (self = [super init]) { //创建缓存录音文件到Tmp NSString *wavRecordFilePath = [self createWaveFilePath]; if (![[NSFileManager defaultManager] fileExistsAtPath:wavRecordFilePath]) { [[NSData data] writeToFile:wavRecordFilePath atomically:YES]; } self.originWaveFilePath = wavRecordFilePath; NSLog(@"ios------初始化默认录制文件路径---%@",wavRecordFilePath); } return self; } - (void) initByMp3{ //创建缓存录音文件到Tmp NSString *mp3RecordFilePath = [self createMp3FilePath]; if (![[NSFileManager defaultManager] fileExistsAtPath:mp3RecordFilePath]) { [[NSData data] writeToFile:mp3RecordFilePath atomically:YES]; } self.originWaveFilePath = mp3RecordFilePath; NSLog(@"ios------初始化录制文件路径---%@",mp3RecordFilePath); } - (NSString *) createMp3FilePath { return [NSTemporaryDirectory() stringByAppendingPathComponent:@"WAVtemporaryRadio.MP3"]; } - (NSString *) createWaveFilePath { return [NSTemporaryDirectory() stringByAppendingPathComponent:@"WAVtemporaryRadio.wav"]; } /// 根据传递过来的文件路径创建wav录制文件路径 /// @param wavPath 传递的文件路径 - (void)initByWavPath:(NSString *) wavPath{ NSString *wavRecordFilePath = wavPath; if (![[NSFileManager defaultManager] fileExistsAtPath:wavRecordFilePath]) { [[NSData data] writeToFile:wavRecordFilePath atomically:YES]; } self.originWaveFilePath = wavRecordFilePath; NSLog(@"ios-----传递的录制文件路径-------- %@",wavRecordFilePath); } /// 开始录制方法 - (void)startRecording { if (isRecording) return; [[DPAudioPlayer sharedInstance]stopPlaying]; //开始录音 [[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayAndRecord error:nil]; // //默认情况下扬声器播放 AVAudioSessionPortOverride portOverride = AVAudioSessionPortOverrideNone; [[AVAudioSession sharedInstance] overrideOutputAudioPort:portOverride error:nil]; [[AVAudioSession sharedInstance] setActive:YES error:nil]; [self.audioRecorder prepareToRecord]; [self.audioRecorder record]; if ([self.audioRecorder isRecording]) { isRecording = YES; [self activeTimer]; if (self.audioStartRecording) { self.audioStartRecording(YES); } } else { if (self.audioStartRecording) { self.audioStartRecording(NO); } } [self createPickSpeakPowerTimer]; } - (void)stopRecording; { if (!isRecording) return; // try!AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.speaker) [self shutDownTimer]; [self.audioRecorder stop]; self.audioRecorder = nil; //设置播放语音为k公开模式 AVAudioSession *avAudioSession = [AVAudioSession sharedInstance]; [avAudioSession overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil]; } - (void)activeTimer { //录音时长 audioTimeLength = 0; NSTimeInterval timeInterval = 0.1; __weak typeof(self) weakSelf = self; [[JX_GCDTimerManager sharedInstance] scheduledDispatchTimerWithName:TimerName timeInterval:timeInterval queue:nil repeats:YES actionOption:AbandonPreviousAction action:^{ __strong typeof(weakSelf) strongSelf = weakSelf; strongSelf->audioTimeLength += timeInterval; if (strongSelf->audioTimeLength >= MAX_RECORDER_TIME) { //大于等于 MAX_RECORDER_TIME 秒停止 [strongSelf stopRecording]; } }]; } - (void)shutDownTimer { [[JX_GCDTimerManager sharedInstance] cancelAllTimer];//定时器停止 } - (AVAudioRecorder *)audioRecorder { if (!_audioRecorder) { //暂存录音文件路径 NSString *wavRecordFilePath = self.originWaveFilePath; NSLog(@"%@", wavRecordFilePath); NSDictionary *param = @{AVSampleRateKey:@8000.0, //采样率 AVFormatIDKey:@(kAudioFormatLinearPCM),//音频格式 AVLinearPCMBitDepthKey:@16, //采样位数 默认 16 AVNumberOfChannelsKey:@1, // 通道的数目 AVEncoderAudioQualityKey:@(AVAudioQualityMin), AVEncoderBitRateKey:@16000, // AVEncoderBitRateStrategyKey:AVAudioBitRateStrategy_VariableConstrained }; NSError *initError; NSURL *fileURL = [NSURL fileURLWithPath:wavRecordFilePath]; _audioRecorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:param error:&initError]; if (initError) { NSLog(@"AVAudioRecorder initError:%@", initError.localizedDescription); } _audioRecorder.delegate = self; _audioRecorder.meteringEnabled = YES; } return _audioRecorder; } #pragma mark - AVAudioRecorder - (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag { //暂存录音文件路径 NSString *wavRecordFilePath = self.originWaveFilePath; NSLog(@"录音暂存位置 %@ ",wavRecordFilePath); NSData *cacheAudioData; switch (preferredVoiceType) { case CSVoiceTypeWav: cacheAudioData = [NSData dataWithContentsOfFile:wavRecordFilePath]; break; } //大于最小录音时长时,发送数据 if (audioTimeLength > MIN_RECORDER_TIME) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSUInteger location = 4100; NSData *body = [cacheAudioData subdataWithRange:NSMakeRange(location, cacheAudioData.length - location)]; NSMutableData *data1 = WriteWavFileHeader(body.length + 44, 8000, 1, 16).mutableCopy; [data1 appendData:body]; // NSLog(@"date1date1date1date1[0-200]:%@", [data1 subdataWithRange:NSMakeRange(0, 200)]); dispatch_sync(dispatch_get_main_queue(), ^{ if (self.audioRecorderFinishRecording) { self.audioRecorderFinishRecording(data1, self->audioTimeLength,wavRecordFilePath); } }); }); } else { if (self.audioRecordingFail) { self.audioRecordingFail(@"录音时长小于设定最短时长"); } } isRecording = NO; //取消定时器 if (timer) { dispatch_source_cancel(timer); timer = NULL; } } NSData* WriteWavFileHeader(long lengthWithHeader, int sampleRate, int channels, int PCMBitDepth) { Byte header[44]; header[0] = 'R'; // RIFF/WAVE header header[1] = 'I'; header[2] = 'F'; header[3] = 'F'; long totalDataLen = lengthWithHeader - 8; header[4] = (Byte) (totalDataLen & 0xff); //file-size (equals file-size - 8) header[5] = (Byte) ((totalDataLen >> 8) & 0xff); header[6] = (Byte) ((totalDataLen >> 16) & 0xff); header[7] = (Byte) ((totalDataLen >> 24) & 0xff); header[8] = 'W'; // Mark it as type "WAVE" header[9] = 'A'; header[10] = 'V'; header[11] = 'E'; header[12] = 'f'; // Mark the format section 'fmt ' chunk header[13] = 'm'; header[14] = 't'; header[15] = ' '; header[16] = 16; // 4 bytes: size of 'fmt ' chunk, Length of format data. Always 16 header[17] = 0; header[18] = 0; header[19] = 0; header[20] = 1; // format = 1 ,Wave type PCM header[21] = 0; header[22] = (Byte) channels; // channels header[23] = 0; header[24] = (Byte) (sampleRate & 0xff); header[25] = (Byte) ((sampleRate >> 8) & 0xff); header[26] = (Byte) ((sampleRate >> 16) & 0xff); header[27] = (Byte) ((sampleRate >> 24) & 0xff); int byteRate = sampleRate * channels * PCMBitDepth >> 3; header[28] = (Byte) (byteRate & 0xff); header[29] = (Byte) ((byteRate >> 8) & 0xff); header[30] = (Byte) ((byteRate >> 16) & 0xff); header[31] = (Byte) ((byteRate >> 24) & 0xff); header[32] = (Byte) (channels * PCMBitDepth >> 3); // block align header[33] = 0; header[34] = PCMBitDepth; // bits per sample header[35] = 0; header[36] = 'd'; //"data" marker header[37] = 'a'; header[38] = 't'; header[39] = 'a'; long totalAudioLen = lengthWithHeader - 44; header[40] = (Byte) (totalAudioLen & 0xff); //data-size (equals file-size - 44). header[41] = (Byte) ((totalAudioLen >> 8) & 0xff); header[42] = (Byte) ((totalAudioLen >> 16) & 0xff); header[43] = (Byte) ((totalAudioLen >> 24) & 0xff); return [[NSData alloc] initWithBytes:header length:44];; } //音频值测量 - (void)createPickSpeakPowerTimer { timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC, 1ull * NSEC_PER_SEC); __weak __typeof(self) weakSelf = self; dispatch_source_set_event_handler(timer, ^{ __strong __typeof(weakSelf) _self = weakSelf; [_self->_audioRecorder updateMeters]; double lowPassResults = pow(10, (0.05 * [_self->_audioRecorder averagePowerForChannel:0])); if (_self.audioSpeakPower) { _self.audioSpeakPower(lowPassResults); } }); dispatch_resume(timer); } - (void)dealloc { if (isRecording) [self.audioRecorder stop]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end ================================================ FILE: ios/Classes/FlutterPluginRecordPlugin.h ================================================ #import @interface FlutterPluginRecordPlugin : NSObject @end ================================================ FILE: ios/Classes/FlutterPluginRecordPlugin.m ================================================ #import "FlutterPluginRecordPlugin.h" #import "DPAudioRecorder.h" #import "DPAudioPlayer.h" @implementation FlutterPluginRecordPlugin{ FlutterMethodChannel *_channel; FlutterResult _result; FlutterMethodCall *_call; NSData *wavData; NSString *audioPath; BOOL _isInit;//是否执行初始化的标识 } + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"flutter_plugin_record" binaryMessenger:[registrar messenger]]; FlutterPluginRecordPlugin *instance = [[FlutterPluginRecordPlugin alloc] initWithChannel:channel]; [registrar addMethodCallDelegate:instance channel:channel]; } - (instancetype)initWithChannel:(FlutterMethodChannel *)channel { self = [super init]; if (self) { _channel = channel; _isInit = NO; } return self; } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result{ _result = result; _call = call; NSString *method = call.method; if ([@"init" isEqualToString:method]) { [self initRecord ]; }else if([@"initRecordMp3" isEqualToString:method]){ [self initMp3Record]; }else if([@"startByWavPath" isEqualToString:method]){ [self startByWavPath]; }else if([@"start" isEqualToString:method]){ [self start ]; }else if([@"stop" isEqualToString:method]){ [self stop ]; }else if([@"play" isEqualToString:method]){ [self play ]; }else if([@"pause" isEqualToString:method]){ [self pausePlay ]; }else if([@"playByPath" isEqualToString:method]){ [self playByPath]; }else if([@"stopPlay" isEqualToString:method]){ [self stopPlay]; }else{ result(FlutterMethodNotImplemented); } } //初始化录制mp3 - (void) initMp3Record{ [DPAudioRecorder.sharedInstance initByMp3]; [self initRecord]; } ///初始化语音录制的方法 初始化录制完成的回调,开始录制的回调,录制失败的回调,录制音量大小的回调 /// 注意未初始化的话 Flutter 不能监听到上述回调事件 - (void) initRecord{ _isInit = YES; DPAudioRecorder.sharedInstance.audioRecorderFinishRecording = ^void (NSData *data, NSTimeInterval audioTimeLength,NSString *path){ self->audioPath =path; self->wavData = data; NSLog(@"ios voice onStop"); NSDictionary *args = [self->_call arguments]; NSString *mId = [args valueForKey:@"id"]; NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys: @"success", @"result", mId, @"id", path, @"voicePath", [NSString stringWithFormat:@"%.20lf", audioTimeLength], @"audioTimeLength", nil]; [self->_channel invokeMethod:@"onStop" arguments:dict3]; }; DPAudioRecorder.sharedInstance.audioStartRecording = ^void(BOOL isRecording){ NSLog(@"ios voice start audioStartRecording"); }; DPAudioRecorder.sharedInstance.audioRecordingFail = ^void(NSString *reason){ NSLog(@"ios voice %@", reason); }; DPAudioRecorder.sharedInstance.audioSpeakPower = ^void(float power){ NSLog(@"ios voice %f",power); NSString *powerStr = [NSString stringWithFormat:@"%f", power]; NSDictionary *args = [self->_call arguments]; NSString *mId = [args valueForKey:@"id"]; NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys: @"success",@"result", mId ,@"id", powerStr,@"amplitude", nil]; [self->_channel invokeMethod:@"onAmplitude" arguments:dict3]; }; NSLog(@"ios voice init"); NSDictionary *args = [_call arguments]; NSString *mId = [args valueForKey:@"id"]; NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:@"success",@"result",mId,@"id", nil]; [_channel invokeMethod:@"onInit" arguments:dict3]; } /// 开始录制的方法 - (void) start{ if (!_isInit) { NSLog(@"ios-------未初始化录制方法- initRecord--"); return; } NSLog(@"ios--------start record -----function--- start----"); [DPAudioRecorder.sharedInstance startRecording]; NSDictionary *args = [_call arguments]; NSString *mId = [args valueForKey:@"id"]; NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:@"success",@"result",mId,@"id", nil]; [_channel invokeMethod:@"onStart" arguments:dict3]; } /// 根据文件路径进行录制 - (void) startByWavPath{ if (!_isInit) { NSLog(@"ios-------未初始化录制方法- initRecord--"); return; } NSDictionary *args = [_call arguments]; NSString *mId = [args valueForKey:@"id"]; NSString *wavPath = [args valueForKey:@"wavPath"]; NSLog(@"ios--------start record -----function--- startByWavPath----%@", wavPath); [DPAudioRecorder.sharedInstance initByWavPath:wavPath]; [DPAudioRecorder.sharedInstance startRecording]; NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:@"success",@"result",mId,@"id", nil]; [_channel invokeMethod:@"onStart" arguments:dict3]; } /// 停止录制的方法 - (void) stop{ if (!_isInit) { NSLog(@"ios-------未初始化录制方法- initRecord--"); return; } NSLog(@"ios--------stop record -----function--- stop----"); [DPAudioRecorder.sharedInstance stopRecording]; } /// 播放录制完成的音频 - (void) play{ NSLog(@"ios------play voice by warData----function---play--"); [DPAudioPlayer.sharedInstance startPlayWithData:self->wavData]; DPAudioPlayer.sharedInstance.playComplete = ^void(){ NSLog(@"ios-----播放完成----by play"); NSDictionary *args = [self->_call arguments]; NSString *mId = [args valueForKey:@"id"]; NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:self->audioPath,@"playPath",@"complete",@"playState",mId,@"id", nil]; [self->_channel invokeMethod:@"onPlayState" arguments:dict3]; }; NSDictionary *args = [_call arguments]; NSString *mId = [args valueForKey:@"id"]; NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:@"success",@"result",mId,@"id", nil]; [_channel invokeMethod:@"onPlay" arguments:dict3]; } - (void)stopPlay{ [DPAudioPlayer.sharedInstance stopPlaying]; } - (void) pausePlay{ NSLog(@"ios------pausePlay----function---pausePlay--"); bool isPlaying = [DPAudioPlayer.sharedInstance pausePlaying]; NSDictionary *args = [_call arguments]; NSString *mId = [args valueForKey:@"id"]; NSString *isPlayingStr = nil; if (isPlaying) { isPlayingStr = @"true"; }else{ isPlayingStr = @"false"; } NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys: @"success",@"result", isPlayingStr,@"isPlaying", mId,@"id", nil]; [_channel invokeMethod:@"pausePlay" arguments:dict3]; } /// 根据指定路径播放音频 - (void) playByPath{ NSLog(@"ios------play voice by path-----function---playByPath---"); NSDictionary *args = [_call arguments]; NSString *filePath = [args valueForKey:@"path"]; NSString *typeStr = [args valueForKey:@"type"]; NSData *data; if ([typeStr isEqualToString:@"url"]) { data =[[NSData alloc]initWithContentsOfURL:[NSURL URLWithString:filePath]]; }else if([typeStr isEqualToString:@"file"]){ data= [NSData dataWithContentsOfFile:filePath]; } [DPAudioPlayer.sharedInstance startPlayWithData:data]; DPAudioPlayer.sharedInstance.playComplete = ^void(){ NSLog(@"ios-----播放完成----by playbyPath---"); NSDictionary *args = [self->_call arguments]; NSString *mId = [args valueForKey:@"id"]; NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:filePath,@"playPath",@"complete",@"playState",mId,@"id", nil]; [self->_channel invokeMethod:@"onPlayState" arguments:dict3]; }; NSString *mId = [args valueForKey:@"id"]; NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:@"success",@"result",mId,@"id", nil]; [_channel invokeMethod:@"onPlay" arguments:dict3]; } @end ================================================ FILE: ios/Classes/JX_GCDTimerManager.h ================================================ #import typedef enum : NSUInteger { AbandonPreviousAction, // 废除之前的任务 MergePreviousAction // 将之前的任务合并到新的任务中 } ActionOption; @interface JX_GCDTimerManager : NSObject + (JX_GCDTimerManager *)sharedInstance; /** 启动一个timer,默认精度为0.1秒 @param timerName timer的名称,作为唯一标识 @param interval 执行的时间间隔 @param queue timer将被放入的队列,也就是最终action执行的队列。传入nil将自动放到一个子线程队列中 @param repeats timer是否循环调用 @param option 多次schedule同一个timer时的操作选项(目前提供将之前的任务废除或合并的选项) @param action 时间间隔到点时执行的block */ - (void)scheduledDispatchTimerWithName:(NSString *)timerName timeInterval:(double)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats actionOption:(ActionOption)option action:(dispatch_block_t)action; /** 撤销某个timer @param timerName timer的名称,作为唯一标识 */ - (void)cancelTimerWithName:(NSString *)timerName; /** 撤销所有的timer */ - (void)cancelAllTimer; @end ================================================ FILE: ios/Classes/JX_GCDTimerManager.m ================================================ #import "JX_GCDTimerManager.h" @interface JX_GCDTimerManager() @property (nonatomic, strong) NSMutableDictionary *timerContainer; @property (nonatomic, strong) NSMutableDictionary *actionBlockCache; @end @implementation JX_GCDTimerManager #pragma mark - Public Method + (JX_GCDTimerManager *)sharedInstance { static JX_GCDTimerManager *_gcdTimerManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken,^{ _gcdTimerManager = [[JX_GCDTimerManager alloc] init]; }); return _gcdTimerManager; } - (void)scheduledDispatchTimerWithName:(NSString *)timerName timeInterval:(double)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats actionOption:(ActionOption)option action:(dispatch_block_t)action { if (nil == timerName) return; if (nil == queue) queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_source_t timer = [self.timerContainer objectForKey:timerName]; if (!timer) { timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_resume(timer); [self.timerContainer setObject:timer forKey:timerName]; } /* timer精度为0.1秒 */ dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC); __weak typeof(self) weakSelf = self; switch (option) { case AbandonPreviousAction: { /* 移除之前的action */ [weakSelf removeActionCacheForTimer:timerName]; dispatch_source_set_event_handler(timer, ^{ action(); if (!repeats) { [weakSelf cancelTimerWithName:timerName]; } }); } break; case MergePreviousAction: { /* cache本次的action */ [self cacheAction:action forTimer:timerName]; dispatch_source_set_event_handler(timer, ^{ NSMutableArray *actionArray = [self.actionBlockCache objectForKey:timerName]; [actionArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { dispatch_block_t actionBlock = obj; actionBlock(); }]; [weakSelf removeActionCacheForTimer:timerName]; if (!repeats) { [weakSelf cancelTimerWithName:timerName]; } }); } break; } } - (void)cancelTimerWithName:(NSString *)timerName { dispatch_source_t timer = [self.timerContainer objectForKey:timerName]; if (!timer) { return; } [self.timerContainer removeObjectForKey:timerName]; dispatch_source_cancel(timer); [self.actionBlockCache removeObjectForKey:timerName]; } - (void)cancelAllTimer { // Fast Enumeration [self.timerContainer enumerateKeysAndObjectsUsingBlock:^(NSString *timerName, dispatch_source_t timer, BOOL *stop) { [self.timerContainer removeObjectForKey:timerName]; dispatch_source_cancel(timer); }]; } #pragma mark - Property - (NSMutableDictionary *)timerContainer { if (!_timerContainer) { _timerContainer = [[NSMutableDictionary alloc] init]; } return _timerContainer; } - (NSMutableDictionary *)actionBlockCache { if (!_actionBlockCache) { _actionBlockCache = [[NSMutableDictionary alloc] init]; } return _actionBlockCache; } #pragma mark - Private Method - (void)cacheAction:(dispatch_block_t)action forTimer:(NSString *)timerName { id actionArray = [self.actionBlockCache objectForKey:timerName]; if (actionArray && [actionArray isKindOfClass:[NSMutableArray class]]) { [(NSMutableArray *)actionArray addObject:action]; }else { NSMutableArray *array = [NSMutableArray arrayWithObject:action]; [self.actionBlockCache setObject:array forKey:timerName]; } } - (void)removeActionCacheForTimer:(NSString *)timerName { if (![self.actionBlockCache objectForKey:timerName]) return; [self.actionBlockCache removeObjectForKey:timerName]; } @end ================================================ FILE: ios/flutter_plugin_record.podspec ================================================ # # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # #use_frameworks! Pod::Spec.new do |s| s.name = 'flutter_plugin_record' s.version = '0.0.1' s.summary = 'A new Flutter plugin.' s.description = <<-DESC A new Flutter plugin. DESC s.homepage = 'http://example.com' s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } # s.swift_version = '4.2' s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' # s.vendored_libraries = 'Classes/libopencore-amrnb.a' s.dependency 'Flutter' s.framework = "AVFoundation" s.ios.deployment_target = '8.0' end ================================================ FILE: lib/const/play_state.dart ================================================ class PlayState { String playState; String playPath; PlayState(this.playState, this.playPath); } ================================================ FILE: lib/const/record_state.dart ================================================ class RecordState { ///发送到原生端的方法名 static String init = "init"; static String start = "start"; static String startByWavPath = "startByWavPath"; static String stop = "stop"; static String play = "play"; static String playByPath = "playByPath"; ///原生端的回调方法名 static String onInit = "onInit"; static String onStart = "onStart"; static String onStop = "onStop"; static String onPlay = "onPlay"; static String onAmplitude = "onAmplitude"; static String onPlayState = "onPlayState"; } ================================================ FILE: lib/const/response.dart ================================================ class RecordResponse { bool? success; String? path; String? msg; String? key; double? audioTimeLength; RecordResponse( {this.success, this.path, this.msg, this.key, this.audioTimeLength}); // RecordResponse({this.success, this.path,this.msg,this.key}); } ================================================ FILE: lib/flutter_plugin_record.dart ================================================ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter_plugin_record/const/play_state.dart'; import 'package:flutter_plugin_record/const/response.dart'; import 'package:uuid/uuid.dart'; class FlutterPluginRecord { final MethodChannel _channel = const MethodChannel('flutter_plugin_record'); static final _uuid = new Uuid(); String id = ''; static final alis = new Map(); FlutterPluginRecord() { id = _uuid.v4(); alis[id] = this; // print("--------FlutterPluginRecord init"); } ///Flutter 调用原生初始化 Future _invokeMethod(String method, [Map arguments = const {}]) { Map withId = Map.of(arguments); withId['id'] = id; _channel.setMethodCallHandler(_handler); return _channel.invokeMethod(method, withId); } ///初始化init的回调 StreamController _responseInitController = new StreamController.broadcast(); Stream get responseFromInit => _responseInitController.stream; ///开始录制 停止录制的回调监听 StreamController _responseController = new StreamController.broadcast(); Stream get response => _responseController.stream; ///音量高低的回调 StreamController _responseAmplitudeController = new StreamController.broadcast(); Stream get responseFromAmplitude => _responseAmplitudeController.stream; ///播放状态监听 StreamController _responsePlayStateController = new StreamController.broadcast(); Stream get responsePlayStateController => _responsePlayStateController.stream; ///原生回调 static Future _handler(MethodCall methodCall) async{ // print("--------FlutterPluginRecord " + methodCall.method); String id = (methodCall.arguments as Map)['id']; FlutterPluginRecord recordPlugin = alis[id] ?? FlutterPluginRecord(); switch (methodCall.method) { case "onInit": bool flag = false; if ("success" == methodCall.arguments["result"]) { flag = true; } recordPlugin._responseInitController.add(flag); break; case "onStart": if ("success" == methodCall.arguments["result"]) { RecordResponse res = new RecordResponse( success: true, path: "", msg: "onStart", key: methodCall.arguments["key"].toString(), ); recordPlugin._responseController.add(res); } break; case "onStop": if ("success" == methodCall.arguments["result"]) { RecordResponse res = new RecordResponse( success: true, path: methodCall.arguments["voicePath"].toString(), audioTimeLength: double.parse(methodCall.arguments["audioTimeLength"]), msg: "onStop", key: methodCall.arguments["key"].toString(), ); recordPlugin._responseController.add(res); } break; case "onPlay": RecordResponse res = new RecordResponse( success: true, path: "", msg: "开始播放", key: methodCall.arguments["key"].toString(), ); recordPlugin._responseController.add(res); break; case "onAmplitude": if ("success" == methodCall.arguments["result"]) { RecordResponse res = new RecordResponse( success: true, path: "", msg: methodCall.arguments["amplitude"].toString(), key: methodCall.arguments["key"].toString(), ); recordPlugin._responseAmplitudeController.add(res); } break; case "onPlayState": var playState = methodCall.arguments["playState"]; var playPath = methodCall.arguments["playPath"]; PlayState res = new PlayState(playState, playPath); recordPlugin._responsePlayStateController.add(res); break; case "pausePlay": //暂停或继续播放 var isPlaying = methodCall.arguments["isPlaying"]; PlayState res = new PlayState(isPlaying, ""); recordPlugin._responsePlayStateController.add(res); break; default: print("default"); break; } return null; } //初始化 Future init() async { return await _invokeMethod('init', { "init": "init", }); } //初始化 Future initRecordMp3() async { return await _invokeMethod('initRecordMp3', { "initRecordMp3": "initRecordMp3", }); } Future start() async { return await _invokeMethod('start', { "start": "start", }); } Future startByWavPath(String wavPath) async { return await _invokeMethod('startByWavPath', { "wavPath": wavPath, }); } Future stop() async { return await _invokeMethod('stop', { "stop": "stop", }); } Future play() async { return await _invokeMethod('play', { "play": "play", }); } // Future playByPath(String path) async { // return await _invokeMethod('playByPath', { // "play": "play", // "path": path, // }); // } /// /// 参数 path 播放音频的地址 /// ///path 为 url类型对应的在线播放地址 https://linjuli-app-audio.oss-cn-hangzhou.aliyuncs.com/audio/50c39c768b534ce1ba25d837ed153824.wav ///path 对应本地文件路径对应的是本地文件播放肚子 /sdcard/flutterdemo/wiw.wav /// 参数 type /// 当path 为url type为 url /// 当path 为本地地址 type为 file /// Future playByPath(String path, String type) async { return await _invokeMethod('playByPath', { "play": "play", "path": path, "type": type, }); } ///暂停播放 Future pausePlay() async { return await _invokeMethod('pause', { "pause": "pause", }); } /// 提供停止播放的功能 Future stopPlay() async { return await _invokeMethod('stopPlay', {}); } dispose() { // stopPlay(); _responseInitController.close(); _responseController.close(); _responseAmplitudeController.close(); _responsePlayStateController.close(); } } ================================================ FILE: lib/index.dart ================================================ export 'const/record_state.dart'; export 'flutter_plugin_record.dart'; export 'utils/common_toast.dart'; export 'widgets/custom_overlay.dart'; export 'widgets/voice_widget.dart'; ================================================ FILE: lib/utils/common_toast.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_plugin_record/widgets/custom_overlay.dart'; class CommonToast { static showView({ BuildContext? context, String? msg, TextStyle? style, Widget? icon, Duration duration = const Duration(seconds: 1), int count = 3, Function? onTap, }) { OverlayEntry? overlayEntry; int _count = 0; void removeOverlay() { overlayEntry?.remove(); overlayEntry = null; } if (overlayEntry == null) { overlayEntry = new OverlayEntry(builder: (content) { return Container( child: GestureDetector( onTap: () { if (onTap != null) { removeOverlay(); onTap(); } }, child: CustomOverlay( icon: Column( children: [ Padding( child: icon, padding: const EdgeInsets.only( bottom: 10.0, ), ), Container( // padding: EdgeInsets.only(right: 20, left: 20, top: 0), child: Text( msg ?? '', style: style ?? TextStyle( fontStyle: FontStyle.normal, color: Colors.white, fontSize: 16, ), ), ) ], ), ), ), ); }); Overlay.of(context!)!.insert(overlayEntry!); if (onTap != null) return; Timer.periodic(duration, (timer) { _count++; if (_count == count) { _count = 0; timer.cancel(); removeOverlay(); } }); } } } ================================================ FILE: lib/widgets/custom_overlay.dart ================================================ import 'package:flutter/material.dart'; class CustomOverlay extends StatelessWidget { final Widget? icon; final BoxDecoration decoration; final double width; final double height; const CustomOverlay({ Key? key, this.icon, this.decoration = const BoxDecoration( color: Color(0xff77797A), borderRadius: BorderRadius.all(Radius.circular(20.0)), ), this.width = 160, this.height = 160, }) : super(key: key); @override Widget build(BuildContext context) { return Positioned( top: MediaQuery.of(context).size.height * 0.5 - width / 2, left: MediaQuery.of(context).size.width * 0.5 - height / 2, child: Material( type: MaterialType.transparency, child: Center( child: Opacity( opacity: 0.8, child: Container( width: width, height: height, decoration: decoration, child: icon, ), ), ), ), ); } } ================================================ FILE: lib/widgets/voice_widget.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_plugin_record/flutter_plugin_record.dart'; import 'package:flutter_plugin_record/utils/common_toast.dart'; import 'custom_overlay.dart'; typedef startRecord = Future Function(); typedef stopRecord = Future Function(); class VoiceWidget extends StatefulWidget { final Function? startRecord; final Function? stopRecord; final double? height; final EdgeInsets? margin; final Decoration? decoration; /// startRecord 开始录制回调 stopRecord回调 const VoiceWidget( {Key? key, this.startRecord, this.stopRecord, this.height, this.decoration, this.margin}) : super(key: key); @override _VoiceWidgetState createState() => _VoiceWidgetState(); } class _VoiceWidgetState extends State { // 倒计时总时长 int _countTotal = 12; double starty = 0.0; double offset = 0.0; bool isUp = false; String textShow = "按住说话"; String toastShow = "手指上滑,取消发送"; String voiceIco = "images/voice_volume_1.png"; ///默认隐藏状态 bool voiceState = true; FlutterPluginRecord? recordPlugin; Timer? _timer; int _count = 0; OverlayEntry? overlayEntry; @override void initState() { super.initState(); recordPlugin = new FlutterPluginRecord(); _init(); ///初始化方法的监听 recordPlugin?.responseFromInit.listen((data) { if (data) { print("初始化成功"); } else { print("初始化失败"); } }); /// 开始录制或结束录制的监听 recordPlugin?.response.listen((data) { if (data.msg == "onStop") { ///结束录制时会返回录制文件的地址方便上传服务器 print("onStop " + data.path!); if (widget.stopRecord != null) widget.stopRecord!(data.path, data.audioTimeLength); } else if (data.msg == "onStart") { print("onStart --"); if (widget.startRecord != null) widget.startRecord!(); } }); ///录制过程监听录制的声音的大小 方便做语音动画显示图片的样式 recordPlugin!.responseFromAmplitude.listen((data) { var voiceData = double.parse(data.msg ?? ''); setState(() { if (voiceData > 0 && voiceData < 0.1) { voiceIco = "images/voice_volume_2.png"; } else if (voiceData > 0.2 && voiceData < 0.3) { voiceIco = "images/voice_volume_3.png"; } else if (voiceData > 0.3 && voiceData < 0.4) { voiceIco = "images/voice_volume_4.png"; } else if (voiceData > 0.4 && voiceData < 0.5) { voiceIco = "images/voice_volume_5.png"; } else if (voiceData > 0.5 && voiceData < 0.6) { voiceIco = "images/voice_volume_6.png"; } else if (voiceData > 0.6 && voiceData < 0.7) { voiceIco = "images/voice_volume_7.png"; } else if (voiceData > 0.7 && voiceData < 1) { voiceIco = "images/voice_volume_7.png"; } else { voiceIco = "images/voice_volume_1.png"; } if (overlayEntry != null) { overlayEntry!.markNeedsBuild(); } }); print("振幅大小 " + voiceData.toString() + " " + voiceIco); }); } ///显示录音悬浮布局 buildOverLayView(BuildContext context) { if (overlayEntry == null) { overlayEntry = new OverlayEntry(builder: (content) { return CustomOverlay( icon: Column( children: [ Container( margin: const EdgeInsets.only(top: 10), child: _countTotal - _count < 11 ? Center( child: Padding( padding: const EdgeInsets.only(bottom: 15.0), child: Text( (_countTotal - _count).toString(), style: TextStyle( fontSize: 70.0, color: Colors.white, ), ), ), ) : new Image.asset( voiceIco, width: 100, height: 100, package: 'flutter_plugin_record', ), ), Container( // padding: const EdgeInsets.only(right: 20, left: 20, top: 0), child: Text( toastShow, style: TextStyle( fontStyle: FontStyle.normal, color: Colors.white, fontSize: 14, ), ), ) ], ), ); }); Overlay.of(context)!.insert(overlayEntry!); } } showVoiceView() { setState(() { textShow = "松开结束"; voiceState = false; }); ///显示录音悬浮布局 buildOverLayView(context); start(); } hideVoiceView() { if (_timer!.isActive) { if (_count < 1) { CommonToast.showView( context: context, msg: '说话时间太短', icon: Text( '!', style: TextStyle(fontSize: 80, color: Colors.white), )); isUp = true; } _timer?.cancel(); _count = 0; } setState(() { textShow = "按住说话"; voiceState = true; }); stop(); if (overlayEntry != null) { overlayEntry?.remove(); overlayEntry = null; } if (isUp) { print("取消发送"); } else { print("进行发送"); } } moveVoiceView() { // print(offset - start); setState(() { isUp = starty - offset > 100 ? true : false; if (isUp) { textShow = "松开手指,取消发送"; toastShow = textShow; } else { textShow = "松开结束"; toastShow = "手指上滑,取消发送"; } }); } ///初始化语音录制的方法 void _init() async { recordPlugin?.init(); } ///开始语音录制的方法 void start() async { recordPlugin?.start(); } ///停止语音录制的方法 void stop() { recordPlugin?.stop(); } @override Widget build(BuildContext context) { return Container( child: GestureDetector( onLongPressStart: (details) { starty = details.globalPosition.dy; _timer = Timer.periodic(Duration(milliseconds: 1000), (t) { _count++; print('_count is 👉 $_count'); if (_count == _countTotal) { hideVoiceView(); } }); showVoiceView(); }, onLongPressEnd: (details) { hideVoiceView(); }, onLongPressMoveUpdate: (details) { offset = details.globalPosition.dy; moveVoiceView(); }, child: Container( height: widget.height ?? 60, // color: Colors.blue, decoration: widget.decoration ?? BoxDecoration( borderRadius: new BorderRadius.circular(6.0), border: Border.all(width: 1.0, color: Colors.grey.shade200), ), margin: widget.margin ?? EdgeInsets.fromLTRB(50, 0, 50, 20), child: Center( child: Text( textShow, ), ), ), ), ); } @override void dispose() { recordPlugin?.dispose(); _timer?.cancel(); super.dispose(); } } ================================================ FILE: pubspec.yaml ================================================ name: flutter_plugin_record description: The flutter voice recording plug-in,provides the recording animation and the recording successfully returns to the recording file path version: 1.0.1 #author: wilson homepage: https://github.com/yxwandroid/flutter_plugin_record #publish_to: none environment: sdk: ">=2.12.0-29.10.beta <3.0.0" flutter: ">=1.12.0" dependencies: flutter: sdk: flutter uuid: ^3.0.0 dev_dependencies: flutter_test: sdk: flutter # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: # This section identifies this Flutter project as a plugin project. # The androidPackage and pluginClass identifiers should not ordinarily # be modified. They are used by the tooling to maintain consistency when # adding or updating assets for this project. plugin: # androidPackage: record.wilson.flutter.com.flutter_plugin_record # pluginClass: FlutterPluginRecordPlugin platforms: android: package: record.wilson.flutter.com.flutter_plugin_record pluginClass: FlutterPluginRecordPlugin ios: pluginClass: FlutterPluginRecordPlugin uses-material-design: true assets: - images/voice_volume_1.png - images/voice_volume_2.png - images/voice_volume_3.png - images/voice_volume_4.png - images/voice_volume_5.png - images/voice_volume_6.png - images/voice_volume_7.png # To add assets to your plugin package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # # For details regarding assets in packages, see # https://flutter.dev/assets-and-images/#from-packages # # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # To add custom fonts to your plugin package, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts in packages, see # https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: test/flutter_plugin_record_test.dart ================================================ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { const MethodChannel channel = MethodChannel('flutter_plugin_record'); setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) async { return '42'; }); }); tearDown(() { channel.setMockMethodCallHandler(null); }); // test('getPlatformVersion', () async { // expect(await FlutterPluginRecord.platformVersion, '42'); // }); }