flutter插件:录制系统播放的声音

该插件基于flutter包 flutter_screen_recording 和 github库 SystemAudioCaptureAndroid,实现了在安卓手机上录制系统播放声音的功能,也就是说,只要一个安卓应用没有设置不允许其它应用录制声音,该插件可以录制该应用播放的声音。

Github 地址:flutterSystemAudioRecorder

创建工程

创建插件工程

flutter create -t plugin --platform android system_audio_recorder

创建好的插件文件夹如下图所示
插件文件夹

创建好的插件工程主要需要修改代码的是以下三个目录

  • android:Android的原生代码
  • example:一个Flutter的实例项目,用来展示、测试你开发的plugin的
  • lib:Plugin的Dart代码

原生代码

用android studio 打开 system_audio_recorder/android,开始修改配置。打开 system_audio_recorder/android/gradle/wrapper/gradle-warpper.properties,将 distributionUrl 修改为 file:///D://work//app//gradle-7.5-all.zip(需要根据实际情况修改,如果网络条件好可以不改)。打开 system_audio_recorder/android/build.gradle 在文件末尾添加flutter和androidx.core的配置

//获取local.properties配置文件  
def localProperties = new Properties()  
def localPropertiesFile = rootProject.file('local.properties')  
if (localPropertiesFile.exists()) {  
    localPropertiesFile.withReader('UTF-8') { reader ->  
        localProperties.load(reader)  
    }  
}  
//获取flutter的sdk路径  
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.")  
}  
  
dependencies {  
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"  
    compileOnly files("$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar")  
    compileOnly 'androidx.annotation:annotation:1.1.0'  
    implementation 'androidx.core:core:1.6.0'
}

点击 sync Now,同步gradle包。

kotlin/com/example/system_audio_recorder/ 已经有 SystemAudioRecorderPlugin.kt ,这个文件用来实现插件的各种方法,但是由于需要录音系统声音,需要使用前台服务,所以需要额外添加一个 ForegroundService.kt 文件用于配置前台服务,ForegroundService.kt 的内容如下

ForegroundService 的package不能和SystemAudioRecorderPlugin一致,否则在启动服务时会报错!!!

package com.foregroundservice  
  
import android.Manifest  
import android.app.Activity  
import android.app.NotificationChannel  
import android.app.NotificationManager  
import android.app.PendingIntent  
import android.app.Service  
import android.content.Context  
import android.content.Intent  
import android.content.pm.PackageManager  
import android.os.Build  
import android.os.IBinder  
import android.util.Log  
import androidx.core.app.ActivityCompat  
import androidx.core.app.NotificationCompat  
import androidx.core.content.ContextCompat  
import com.example.system_audio_recorder.SystemAudioRecorderPlugin  
  
class ForegroundService : Service() {  
    private val CHANNEL_ID = "ForegroundService Kotlin"  
    private val REQUEST_CODE_MEDIA_PROJECTION = 1001  
    // 静态方法,SystemAudioRecorderPlugin 会调用这些方法  
    companion object {  
        fun startService(context: Context, title: String, message: String) {  
            println("-------------------------- startService");  
            try {  
                val startIntent = Intent(context, ForegroundService::class.java)  
                startIntent.putExtra("messageExtra", message)  
                startIntent.putExtra("titleExtra", title)  
                println("-------------------------- startService2");  
  
                ContextCompat.startForegroundService(context, startIntent)  
                println("-------------------------- startService3");  
  
            } catch (err: Exception) {  
                println("startService err");  
                println(err);  
            }  
        }  
  
        fun stopService(context: Context) {  
            val stopIntent = Intent(context, ForegroundService::class.java)  
            context.stopService(stopIntent)  
        }  
    }  
    // 在 SystemAudioRecorderPlugin 调用 ActivityCompat.startActivityForResult 时调用  
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {  
        try {  
            Log.i("ForegroundService", "onStartCommand")  
            // Verification permission en Android 14 (SDK 34)  
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {  
                if (ContextCompat.checkSelfPermission(this, Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION)  
                    != PackageManager.PERMISSION_GRANTED) {  
                    Log.i("Foreground","MediaProjection permission not granted, requesting permission")  
  
                    ActivityCompat.requestPermissions(  
                        this as Activity,  
                        arrayOf(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION),  
                        REQUEST_CODE_MEDIA_PROJECTION  
                    )  
                } else {  
                    startForegroundServiceWithNotification(intent)  
                }  
            } else {  
                startForegroundServiceWithNotification(intent)  
            }  
  
            return START_NOT_STICKY  
        } catch (err: Exception) {  
            Log.e("ForegroundService", "onStartCommand error: $err")  
        }  
        return START_STICKY  
    }  
    private fun startForegroundServiceWithNotification(intent: Intent?) {  
  
        createNotificationChannel()  
        val notificationIntent = Intent(this, SystemAudioRecorderPlugin::class.java)  
  
        val pendingIntent = PendingIntent.getActivity(  
            this, 0, notificationIntent, PendingIntent.FLAG_MUTABLE  
        )  
  
        val notification = NotificationCompat.Builder(this, CHANNEL_ID)  
            .setContentIntent(pendingIntent)  
            .build()  
  
        startForeground(1, notification)  
        Log.i("ForegroundService", "startForegroundServiceWithNotification")  
    }  
  
    override fun onBind(intent: Intent): IBinder? {  
        return null  
    }  
  
    private fun createNotificationChannel() {  
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {  
            val serviceChannel = NotificationChannel(  
                CHANNEL_ID, "Foreground Service Channel", NotificationManager.IMPORTANCE_DEFAULT  
            )  
            val manager = getSystemService(NotificationManager::class.java)  
            manager!!.createNotificationChannel(serviceChannel)  
        }  
    }  
  
}

SystemAudioRecorderPlugin 的内容如下

package com.example.system_audio_recorder  
  
import android.annotation.SuppressLint  
import android.app.Activity  
import android.content.Context  
import android.content.Intent  
import android.media.AudioAttributes  
import android.media.AudioFormat  
import android.media.AudioPlaybackCaptureConfiguration  
import android.media.AudioRecord  
import android.media.projection.MediaProjection  
import android.media.projection.MediaProjectionManager  
import android.os.Build  
import android.os.Environment  
import android.util.Log  
import androidx.annotation.RequiresApi  
import androidx.core.app.ActivityCompat  
  
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.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 java.io.DataInputStream  
import java.io.DataOutputStream  
import java.io.File  
import java.io.FileInputStream  
import java.io.FileNotFoundException  
import java.io.FileOutputStream  
import java.io.IOException  
import java.nio.ByteBuffer  
import java.nio.ByteOrder  
import java.text.SimpleDateFormat  
import java.util.Date  
import com.foregroundservice.ForegroundService  
  
/** SystemAudioRecorderPlugin */  
class SystemAudioRecorderPlugin: MethodCallHandler, PluginRegistry.ActivityResultListener, FlutterPlugin,  
  ActivityAware {  
  
  private lateinit var channel : MethodChannel  
  private var mProjectionManager: MediaProjectionManager? = null  
  private var mMediaProjection: MediaProjection? = null  
  private var mFileName: String? = ""  
  private val RECORD_REQUEST_CODE = 333  
  var TAG: String = "system_audio_recorder"  
  
  private lateinit var _result: Result  
  
  private var pluginBinding: FlutterPlugin.FlutterPluginBinding? = null  
  private var activityBinding: ActivityPluginBinding? = null;  
  var recordingThread: Thread? = null  
  private val bufferElements2Record = 1024  
  private val bytesPerElement = 2  
  private var mAudioRecord: AudioRecord? = null  
  private var isRecording: Boolean = false  
  private var RECORDER_SAMPLERATE: Int = 44100  
  val RECORDER_CHANNELS: Int = AudioFormat.CHANNEL_IN_MONO  
  val RECORDER_AUDIO_ENCODING: Int = AudioFormat.ENCODING_PCM_16BIT  
  private var root: File? = null  
  private var cache: File? = null  
  private var rawOutput: File? = null  
  
  override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {  
    pluginBinding = flutterPluginBinding;  
  }  
  
  @RequiresApi(Build.VERSION_CODES.Q)  
  // 在 ForegroundService 的startCommand执行完后执行  
  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {  
    if (requestCode == RECORD_REQUEST_CODE) {  
      if (resultCode == Activity.RESULT_OK) {  
        mMediaProjection = mProjectionManager?.getMediaProjection(resultCode, data!!)  
        startRecording(mMediaProjection!!)  
        _result.success(true)  
        return true  
      } else {  
        _result.success(false)  
      }  
    }  
    return false  
  }  
  
  override fun onMethodCall(call: MethodCall, result: Result) {  
    val appContext = pluginBinding!!.applicationContext  
  
    if (call.method == "getPlatformVersion") {  
      result.success("Android ${Build.VERSION.RELEASE}")  
    } else if (call.method == "startRecord"){  
      try {  
        _result = result  
        val sampleRate = call.argument<Int?>("sampleRate")  
        if (sampleRate != null){  
          RECORDER_SAMPLERATE = sampleRate  
        }  
  
        ForegroundService.startService(appContext, "开始录音", "开始录音")  
        mProjectionManager =  
          appContext.getSystemService(  
            Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager?  
  
        val permissionIntent = mProjectionManager?.createScreenCaptureIntent()  
        Log.i(TAG, "startActivityForResult")  
        // 调用 ForegroundService的 startCommand 方法  
        ActivityCompat.startActivityForResult(  
          activityBinding!!.activity,  
          permissionIntent!!,  
          RECORD_REQUEST_CODE,  
          null  
        )  
      } catch (e: Exception) {  
        Log.e(TAG, "Error onMethodCall startRecord: ${e.message}")  
        result.success(false)  
      }  
    }  
    else if (call.method == "stopRecord"){  
      Log.i(TAG, "stopRecord")  
      try {  
        ForegroundService.stopService(appContext)  
        if (mAudioRecord != null){  
          stop()  
          result.success(mFileName)  
        }else{  
          result.success("")  
        }  
      } catch (e: Exception) {  
        result.success("")  
      }  
    }  
    else {  
      result.notImplemented()  
    }  
  }  
  
  @RequiresApi(api = Build.VERSION_CODES.Q)  
  fun startRecording(mProjection: MediaProjection): Boolean {  
    Log.i(TAG, "startRecording")  
    if (mAudioRecord == null){  
      val config : AudioPlaybackCaptureConfiguration  
      try {  
        config = AudioPlaybackCaptureConfiguration.Builder(mProjection)  
          .addMatchingUsage(AudioAttributes.USAGE_MEDIA)  
          .addMatchingUsage(AudioAttributes.USAGE_GAME)  
          .build()  
      } catch (e: NoClassDefFoundError) {  
        return false  
      }  
      val format = AudioFormat.Builder()  
        .setEncoding(RECORDER_AUDIO_ENCODING)  
        .setSampleRate(RECORDER_SAMPLERATE)  
        .setChannelMask(RECORDER_CHANNELS)  
        .build()  
  
      mAudioRecord = AudioRecord.Builder().setAudioFormat(format).setBufferSizeInBytes(bufferElements2Record).setAudioPlaybackCaptureConfig(config).build()  
      isRecording = true  
      mAudioRecord!!.startRecording()  
  
      createAudioFile()  
  
      recordingThread = Thread({ writeAudioFile() }, "System Audio Capture")  
  
      recordingThread!!.start()  
  
    }  
    return true  
  }  
  
  @Throws(IOException::class)  
  private fun rawToWave(rawFile: File, waveFile: File) {  
    val rawData = ByteArray(rawFile.length().toInt())  
    var input: DataInputStream? = null  
    try {  
      input = DataInputStream(FileInputStream(rawFile))  
      input.read(rawData)  
    } finally {  
      input?.close()  
    }  
  
    var output: DataOutputStream? = null  
    try {  
      output = DataOutputStream(FileOutputStream(waveFile))  
  
      // WAVE header  
      writeString(output, "RIFF") // chunk id  
      writeInt(output, 36 + rawData.size) // chunk size  
      writeString(output, "WAVE") // format  
      writeString(output, "fmt ") // subchunk 1 id  
      writeInt(output, 16) // subchunk 1 size  
      writeShort(output, 1.toShort()) // audio format (1 = PCM)  
      writeShort(output, 1.toShort()) // number of channels  
      writeInt(output, RECORDER_SAMPLERATE) // sample rate  
      writeInt(output, RECORDER_SAMPLERATE) // byte rate  
      writeShort(output, 2.toShort()) // block align  
      writeShort(output, 16.toShort()) // bits per sample  
      writeString(output, "data") // subchunk 2 id  
      writeInt(output, rawData.size) // subchunk 2 size  
      // Audio data (conversion big endian -> little endian)      
      val shorts = ShortArray(rawData.size / 2)  
      ByteBuffer.wrap(rawData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer()[shorts]  
      val bytes = ByteBuffer.allocate(shorts.size * 2)  
      for (s in shorts) {  
        bytes.putShort(s)  
      }  
  
      output.write(fullyReadFileToBytes(rawFile))  
    } finally {  
      output?.close()  
    }  
  }  
  
  @Throws(IOException::class)  
  fun fullyReadFileToBytes(f: File): ByteArray {  
    val size = f.length().toInt()  
    val bytes = ByteArray(size)  
    val tmpBuff = ByteArray(size)  
    val fis = FileInputStream(f)  
    try {  
      var read = fis.read(bytes, 0, size)  
      if (read < size) {  
        var remain = size - read  
        while (remain > 0) {  
          read = fis.read(tmpBuff, 0, remain)  
          System.arraycopy(tmpBuff, 0, bytes, size - remain, read)  
          remain -= read  
        }  
      }  
    } catch (e: IOException) {  
      throw e  
    } finally {  
      fis.close()  
    }  
  
    return bytes  
  }  
  
  private fun createAudioFile() {  
  
    root = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), "/Audio Capture")  
//    val mFilename: String? = pluginBinding!!.applicationContext.externalCacheDir?.absolutePath  
//    root = File(mFilename)  
    cache = File(pluginBinding!!.applicationContext.cacheDir.absolutePath, "/RawData")  
    if (!root!!.exists()) {  
      root!!.mkdir()  
      root!!.setWritable(true)  
    }  
    if (!cache!!.exists()) {  
      cache!!.mkdir()  
      cache!!.setWritable(true)  
      cache!!.setReadable(true)  
    }  
  
    rawOutput = File(cache, "raw.pcm")  
  
    try {  
      rawOutput!!.createNewFile()  
    } catch (e: IOException) {  
      Log.e(TAG, "createAudioFile: $e")  
      e.printStackTrace()  
    }  
  
    Log.d(TAG, "path: " + rawOutput!!.absolutePath)  
  
  }  
  
  @Throws(IOException::class)  
  private fun writeInt(output: DataOutputStream, value: Int) {  
    output.write(value shr 0)  
    output.write(value shr 8)  
    output.write(value shr 16)  
    output.write(value shr 24)  
  }  
  
  @Throws(IOException::class)  
  private fun writeShort(output: DataOutputStream, value: Short) {  
    output.write(value.toInt() shr 0)  
    output.write(value.toInt() shr 8)  
  }  
  
  @Throws(IOException::class)  
  private fun writeString(output: DataOutputStream, value: String) {  
    for (element in value) {  
      output.write(element.code)  
    }  
  }  
  
  private fun shortToByte(data: ShortArray): ByteArray {  
    val arraySize = data.size  
    val bytes = ByteArray(arraySize * 2)  
    for (i in 0 until arraySize) {  
      bytes[i * 2] = (data[i].toInt() and 0x00FF).toByte()  
      bytes[i * 2 + 1] = (data[i].toInt() shr 8).toByte()  
      data[i] = 0  
    }  
    return bytes  
  }  
  
  private fun writeAudioFile() {  
    try {  
      val outputStream = FileOutputStream(rawOutput!!.absolutePath)  
      val data = ShortArray(bufferElements2Record)  
  
      while (isRecording) {  
        mAudioRecord!!.read(data, 0, bufferElements2Record)  
  
        val buffer = ByteBuffer.allocate(8 * 1024)  
  
        outputStream.write(  
          shortToByte(data),  
          0,  
          bufferElements2Record * bytesPerElement  
        )  
      }  
  
      outputStream.close()  
    } catch (e: FileNotFoundException) {  
      Log.e(TAG, "File Not Found: $e")  
      e.printStackTrace()  
    } catch (e: IOException) {  
      Log.e(TAG, "IO Exception: $e")  
      e.printStackTrace()  
    }  
  }  
  
  @SuppressLint("SimpleDateFormat")  
  fun startProcessing() {  
    isRecording = false  
    mAudioRecord!!.stop()  
    mAudioRecord!!.release()  
  
    mFileName = SimpleDateFormat("yyyy-MM-dd hh-mm-ss").format(Date()) + ".mp3"  
  
    //Convert To mp3 from raw data i.e pcm  
    val output = File(root, mFileName)  
  
    try {  
      output.createNewFile()  
    } catch (e: IOException) {  
      e.printStackTrace()  
      Log.e(TAG, "startProcessing: $e")  
    }  
  
    try {  
      rawOutput?.let { rawToWave(it, output) }  
    } catch (e: IOException) {  
      e.printStackTrace()  
    } finally {  
      rawOutput!!.delete()  
    }  
  }  
  
  private fun stop(){  
    startProcessing()  
    if (mAudioRecord != null){  
      mAudioRecord = null  
      recordingThread = null  
    }  
  }  
  
  override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {}  
  
  override fun onAttachedToActivity(binding: ActivityPluginBinding) {  
    activityBinding = binding;  
    channel = MethodChannel(pluginBinding!!.binaryMessenger, "system_audio_recorder")  
    channel.setMethodCallHandler(this)  
    activityBinding!!.addActivityResultListener(this);  
  }  
  
  override fun onDetachedFromActivityForConfigChanges() {}  
  
  override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {  
    activityBinding = binding;  
  }  
  
  override fun onDetachedFromActivity() {}  
}

此外需要配置一下 system_audio_recorder/src/main/AndroidManifest.xml,添加一些权限。

<?xml version="1.0" encoding="utf-8"?>  
<manifest xmlns:android="http://schemas.android.com/apk/res/android"  
    package="com.example.system_audio_recorder">  
  
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />  
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />  
    <uses-permission android:name="android.permission.WRITE_INTERNAL_STORAGE" />  
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />  
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />  
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION" />  
    <uses-permission android:name="android.permission.WAKE_LOCK" />  
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />  
    <uses-permission android:name="android.permission.RECORD_AUDIO" />  
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />  
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />  
  
</manifest>

插件代码

接下来使用 android studio 打开 system_audio_recorder,编写插件代码。

首先配置一下 system_audio_recorder/pubspec.yaml,添加dependencies

dependencies:  
  flutter:  
    sdk: flutter  
  plugin_platform_interface: ^2.0.2 
  # 新添加的dependencies
  flutter_foreground_task: ^6.0.0+1  
  meta: ^1.5.0

system_audio_recorder/lib 中有三个 dart 文件,三个文件的内容为

system_audio_recorder/lib/system_audio_recorder.dart

  
import 'dart:ffi';  
import 'dart:io';  
  
import 'package:flutter/foundation.dart';  
  
import 'system_audio_recorder_platform_interface.dart';  
import 'package:flutter_foreground_task/flutter_foreground_task.dart';  
  
class SystemAudioRecorder {  
  Future<String?> getPlatformVersion() {  
    return SystemAudioRecorderPlatform.instance.getPlatformVersion();  
  }  
  static Future<bool> startRecord(String name, {String? titleNotification, String? messageNotification, int? sampleRate}) async {  
    try {  
      if (titleNotification == null) {  
        titleNotification = "";  
      }  
      if (messageNotification == null) {  
        messageNotification = "";  
      }  
  
      if (sampleRate == null){  
        sampleRate = 44100;  
      }  
      await _maybeStartFGS(titleNotification, messageNotification);  
      final bool start = await SystemAudioRecorderPlatform.instance.startRecord(  
        name,  
        notificationTitle: titleNotification,  
        notificationMessage: messageNotification,  
        sampleRate: sampleRate,  
      );  
  
      return start;  
    } catch (err) {  
      print("startRecord err");  
      print(err);  
    }  
  
    return false;  
  }  
  
  static Future<String> get stopRecord async {  
    try {  
      final String path = await SystemAudioRecorderPlatform.instance.stopRecord;  
      if (!kIsWeb && Platform.isAndroid) {  
        FlutterForegroundTask.stopService();  
      }  
      return path;  
    } catch (err) {  
      print("stopRecord err");  
      print(err);  
    }  
    return "";  
  }  
  
  static _maybeStartFGS(String titleNotification, String messageNotification) {  
    try {  
      if (!kIsWeb && Platform.isAndroid) {  
        FlutterForegroundTask.init(  
          androidNotificationOptions: AndroidNotificationOptions(  
            channelId: 'notification_channel_id',  
            channelName: titleNotification,  
            channelDescription: messageNotification,  
            channelImportance: NotificationChannelImportance.LOW,  
            priority: NotificationPriority.LOW,  
            iconData: const NotificationIconData(  
              resType: ResourceType.mipmap,  
              resPrefix: ResourcePrefix.ic,  
              name: 'launcher',  
            ),  
          ),  
          iosNotificationOptions: const IOSNotificationOptions(  
            showNotification: true,  
            playSound: false,  
          ),  
          foregroundTaskOptions: const ForegroundTaskOptions(  
            interval: 5000,  
            autoRunOnBoot: true,  
            allowWifiLock: true,  
          ),  
        );  
      }  
    } catch (err) {  
      print("_maybeStartFGS err");  
      print(err);  
    }  
  }  
}

system_audio_recorder_method_channel.dart

import 'package:flutter/foundation.dart';  
import 'package:flutter/services.dart';  
  
import 'system_audio_recorder_platform_interface.dart';  
  
/// An implementation of [SystemAudioRecorderPlatform] that uses method channels.  
class MethodChannelSystemAudioRecorder extends SystemAudioRecorderPlatform {  
  /// The method channel used to interact with the native platform.  
    
  final methodChannel = const MethodChannel('system_audio_recorder');  
  
    
  Future<String?> getPlatformVersion() async {  
    final version = await methodChannel.invokeMethod<String>('getPlatformVersion');  
    return version;  
  }  
  
  Future<bool> startRecord(  
      String name, {  
        String notificationTitle = "",  
        String notificationMessage = "",  
        int sampleRate = 44100  
      }) async {  
    final bool start = await methodChannel.invokeMethod('startRecord', {  
      "name": name,  
      "title": notificationTitle,  
      "message": notificationMessage,  
      "sampleRate": sampleRate  
    });  
    return start;  
  }  
  
  
  Future<String> get stopRecord async {  
    final String path = await methodChannel.invokeMethod('stopRecord');  
    return path;  
  }  
}

system_audio_recorder_platform_interface.dart

import 'package:plugin_platform_interface/plugin_platform_interface.dart';  
  
import 'system_audio_recorder_method_channel.dart';  
  
abstract class SystemAudioRecorderPlatform extends PlatformInterface {  
  /// Constructs a SystemAudioRecorderPlatform.  
  SystemAudioRecorderPlatform() : super(token: _token);  
  
  static final Object _token = Object();  
  
  static SystemAudioRecorderPlatform _instance = MethodChannelSystemAudioRecorder();  
  
  /// The default instance of [SystemAudioRecorderPlatform] to use.  
  ///  /// Defaults to [MethodChannelSystemAudioRecorder].  
  static SystemAudioRecorderPlatform get instance => _instance;  
  
  /// Platform-specific implementations should set this with their own  
  /// platform-specific class that extends [SystemAudioRecorderPlatform] when  
  /// they register themselves.  
  static set instance(SystemAudioRecorderPlatform instance) {  
    PlatformInterface.verifyToken(instance, _token);  
    _instance = instance;  
  }  
  
  Future<String?> getPlatformVersion() {  
    throw UnimplementedError('platformVersion() has not been implemented.');  
  }  
  Future<bool> startRecord(  
      String name, {  
        String notificationTitle = "",  
        String notificationMessage = "",  
        int sampleRate = 44100  
      }) {  
    throw UnimplementedError();  
  }  
  
  Future<String> get stopRecord {  
    throw UnimplementedError();  
  }  
}

example 代码

最后用 android studio 打开 system_audio_recorder/example 文件夹,这里需要在system_audio_recorder/example/android/app/src/main/AndroidManifest.xml中添加 service

<application  
    android:label="system_audio_recorder_example"  
    android:name="${applicationName}"  
    android:icon="@mipmap/ic_launcher"> 
     
	<!--添加service-->
    <service  
        android:name="com.foregroundservice.ForegroundService"  
        android:foregroundServiceType="mediaProjection"  
        android:enabled="true"  
        android:exported="false">  
    </service>  
    
    <activity  
        android:name=".MainActivity"
        .....

同时修改 system_audio_recorder\example\android\app\build.gradle 中的 minSdkVersion 为 23

minSdkVersion

最后在 main.dart 中编写开始录音和停止录音的代码即可,录制完成的声音在系统 Music 文件夹的 Audio Capture 文件夹中。

import 'package:flutter/foundation.dart';  
import 'package:flutter/material.dart';  
import 'dart:async';  
  
import 'package:flutter/services.dart';  
import 'package:system_audio_recorder/system_audio_recorder.dart';  
import 'package:permission_handler/permission_handler.dart';  
  
void main() {  
  runApp(const MyApp());  
}  
  
class MyApp extends StatefulWidget {  
  const MyApp({super.key});  
  
    
  State<MyApp> createState() => _MyAppState();  
}  
  
class _MyAppState extends State<MyApp> {  
  String _platformVersion = 'Unknown';  
  final _systemAudioRecorderPlugin = SystemAudioRecorder();  
  requestPermissions() async {  
    if (!kIsWeb) {  
      if (await Permission.storage.request().isDenied) {  
        await Permission.storage.request();  
      }  
      if (await Permission.photos.request().isDenied) {  
        await Permission.photos.request();  
      }  
      if (await Permission.microphone.request().isDenied) {  
        await Permission.microphone.request();  
      }  
    }  
  }  
    
  void initState() {  
    super.initState();  
    requestPermissions();  
    initPlatformState();  
  }  
  
  // Platform messages are asynchronous, so we initialize in an async method.  
  Future<void> initPlatformState() async {  
    String platformVersion;  
    // Platform messages may fail, so we use a try/catch PlatformException.  
    // We also handle the message potentially returning null.    try {  
      platformVersion =  
          await _systemAudioRecorderPlugin.getPlatformVersion() ?? 'Unknown platform version';  
    } on PlatformException {  
      platformVersion = 'Failed to get platform version.';  
    }  
  
    // If the widget was removed from the tree while the asynchronous platform  
    // message was in flight, we want to discard the reply rather than calling    // setState to update our non-existent appearance.    if (!mounted) return;  
  
    setState(() {  
      _platformVersion = platformVersion;  
    });  
  }  
  
    
  Widget build(BuildContext context) {  
    return MaterialApp(  
      home: Scaffold(  
        appBar: AppBar(  
          title: const Text('Plugin example app'),  
        ),  
        body: Column(  
          children: [  
            Text('Running on: $_platformVersion\n'),  
            TextButton(onPressed: () async {  
              bool start = await SystemAudioRecorder.startRecord("test",  
              titleNotification: "titleNotification",  
                messageNotification: "messageNotification",  
                sampleRate: 16000  
              );  
            }, child: const Text("开始录制")),  
            TextButton(onPressed: ()async{  
              String path = await SystemAudioRecorder.stopRecord;  
              print(path);  
            }, child: const Text("停止录制"))  
          ]  
        ),  
      ),  
    );  
  }  
}

效果图如下
效果图
录音存放的位置(mumu模拟器中)

录音存放位置

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/916485.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【论文阅读】WaDec: Decompiling WebAssembly Using Large Language Model

论文阅读笔记:WaDec: Decompiling WebAssembly Using Large Language Model 1. 来源出处 论文标题: WaDec: Decompiling WebAssembly Using Large Language Model作者: Xinyu She, Yanjie Zhao, Haoyu Wang会议: 39th IEEE/ACM International Conference on Automated Softwar…

【安全测试】sqlmap工具(sql注入)学习

前言&#xff1a;sqimap是一个开源的渗透测试工具&#xff0c;它可以自动化检测和利用SQL注入缺陷以及接管数据库服务器的过程。它有一个强大的检测引擎&#xff0c;许多适合于终极渗透测试的小众特性和广泛的开关&#xff0c;从数据库指纹、从数据库获 取数据到访问底层文件系…

Redis环境部署(主从模式、哨兵模式、集群模式)

一、概述 REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统&#xff0c;是跨平台的非关系型数据库。Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库…

【Excel】身份证号最后一位“X”怎么计算

大多数人身份证号最后一位都是数字&#xff0c;但有个别号码最后一位却是“X"。 如果你查百度&#xff0c;会得到如下答案&#xff1a; 当最后一位编码是10的时候&#xff0c;因为多出一位&#xff0c;所以就用X替换。 可大多数人不知道的是&#xff0c;这个10是怎么来的…

Isaac Sim+SKRL机器人并行强化学习

目录 Isaac Sim介绍 OmniIssacGymEnvs安装 SKRL安装与测试 基于UR5的机械臂Reach强化学习测评 机器人控制 OMNI GYM环境编写 SKRL运行文件 训练结果与速度对比 结果分析 运行体验与建议 Isaac Sim介绍 Isaac Sim是英伟达出的一款机器人仿真平台&#xff0c;适用于做机…

Leetcode 743 Network Delay Time

题意&#xff1a;给定n个节点的网络&#xff0c;以及节点之间传输的时间&#xff0c;求从节点k出发传输信息&#xff0c;最少需要多久&#xff0c;所有的节点都能够接收到信息 https://leetcode.com/problems/network-delay-time/description/ 题解&#xff1a;给定一个有向图…

[Android]相关属性功能的裁剪

1.将home界面的search bar 移除 /src/com/android/launcher3/graphics/LauncherPreviewRenderer.java // Add first page QSBif (FeatureFlags.QSB_ON_FIRST_SCREEN) {CellLayout firstScreen mWorkspaceScreens.get(FIRST_SCREEN_ID);View qsb mHomeElementInflater.infla…

qt中ctrl+鼠标左键无法进入

现象&#xff1a;qt中ctrl鼠标左键无法跳转部分函数&#xff0c;例如能跳到textEdit->toPlainText().&#xff0c;但无法跳转到toUtf8();但编译没有问题 排查1&#xff1a;我发现是交叉编译链的问题&#xff0c;使用linux自带就可以进&#xff0c;用ATK-I.MX6U就部分不能进…

【Android】View—基础知识,滑动,弹性滑动

基础知识 什么是View 在 Android 中&#xff0c;View 是用户界面&#xff08;UI&#xff09;中的基本组件&#xff0c;用于绘制图形和处理用户交互。所有的 UI 组件&#xff08;如按钮、文本框、图片等&#xff09;都是 View 的子类。可以说&#xff0c;View 是构建 Android …

2024年十大信创操作系统之中科红旗的红旗 Linux

随着全球信息技术格局的变化与国家信息安全日益重要&#xff0c;操作系统作为计算机硬件与软件之间的中介&#xff0c;逐渐成为了国家竞争力的核心领域之一。尤其是在我国提出自主创新、国产替代的战略背景下&#xff0c;信创&#xff08;信息技术应用创新&#xff09;产业的快…

QT开发笔记之小知识

QCoreApplication::aboutToQuit 主事件循环退出前发出的信号&#xff0c;是程序退出前等待QT线程退出回收资源的神器。 官方帮助文档 [signal] void QCoreApplication::aboutToQuit() 该信号在应用程序即将退出主事件循环时发出&#xff0c;例如&#xff1a;当事件循环级别降至…

Word VBA如何间隔选中多个(非连续)段落

实例需求&#xff1a;Word文档中的有多个段落&#xff0c;段落总数量不确定&#xff0c;现在需要先选中所有基数段落&#xff0c;即&#xff1a;段落1&#xff0c;段落3 … &#xff0c;然后一次性设置粗体格式。 也许有的读者会认为这个无厘头的需求&#xff0c;循环遍历遍历文…

PyAEDT:Ansys Electronics Desktop API 简介

在本文中&#xff0c;我将向您介绍 PyAEDT&#xff0c;这是一个 Python 库&#xff0c;旨在增强您对 Ansys Electronics Desktop 或 AEDT 的体验。PyAEDT 通过直接与 AEDT API 交互来简化脚本编写&#xff0c;从而允许在 Ansys 的电磁、热和机械求解器套件之间无缝集成。通过利…

软件著作权申请教程(超详细)(2024新版)软著申请

目录 一、注册账号与实名登记 二、材料准备 三、申请步骤 1.办理身份 2.软件申请信息 3.软件开发信息 4.软件功能与特点 5.填报完成 一、注册账号与实名登记 首先我们需要在官网里面注册一个账号&#xff0c;并且完成实名认证&#xff0c;一般是注册【个人】的身份。中…

HTTPS详解:加密机制、工作流程、CA证书与中间人攻击防护

文章目录 1. 前言1.1. 什么是HTTPS1.2. 什么是加密1.3. 常见的加密方式① 对称加密② 非对称加密 1.4. 数据摘要&#xff08;数据指纹&#xff09;① 实例&#xff1a;软件分发中的数据摘要 1.5.1 一个小问题 2. HTTPS 工作流程探究2.1. 方案1 - 只使用对称加密2.2. 方案2 - 只…

机器学习基础04

目录 1.朴素贝叶斯-分类 1.1贝叶斯分类理论 1.2条件概率 1.3全概率公式 1.4贝叶斯推断 1.5朴素贝叶斯推断 1.6拉普拉斯平滑系数 1.7API 2.决策树-分类 2.1决策树 2.2基于信息增益的决策树建立 2.2.1信息熵 2.2.2信息增益 2.2.3信息增益决策树建立步骤 2.3基于基…

【Python · PyTorch】卷积神经网络(基础概念)

【Python PyTorch】卷积神经网络 CNN&#xff08;基础概念&#xff09; 0. 生物学相似性1. 概念1.1 定义1.2 优势1.2.1 权重共享1.2.2 局部连接1.2.3 层次结构 1.3 结构1.4 数据预处理1.4.1 标签编码① One-Hot编码 / 独热编码② Word Embedding / 词嵌入 1.4.2 归一化① Min-…

ospf排错学习

排错步骤是 1、查看ospf的router-id是否相同 2、错误配置ospf发布路由 //典型错误 3、错误的ospf区域号 4、错误的被动接口设置 //接口设置为被动接口&#xff0c;不学习了 排错思路&#xff08;思科命令&#xff09…

AR眼镜方案_AR智能眼镜阵列/衍射光波导显示方案

在当今AR智能眼镜的发展中&#xff0c;显示和光学组件成为了技术攻坚的主要领域。由于这些组件的高制造难度和成本&#xff0c;其光学显示模块在整个设备的成本中约占40%。 采用光波导技术的AR眼镜显示方案&#xff0c;核心结构通常由光机、波导和耦合器组成。光机内的微型显示…

【Linux】多线程(中)

目录 一、线程互斥 1.1 互斥概念 1.2 互斥量mutex 1.3 互斥量相关API &#xff08;1&#xff09;初始化互斥量 &#xff08;2&#xff09;销毁互斥量 &#xff08;3&#xff09;互斥量加锁和解锁 1.4 互斥量原理 1.5 重入和线程安全 二、死锁 2.1 概念 2.2 造成死锁…