Flutter来电录音功能异常:无录音文件生成
Flutter呼入通话无录音文件问题排查与解决
我开发的Flutter应用使用phone_state和record包实现呼入、呼出通话录音,要求保存为3gp格式,但接收来电时没有任何录音内容,也不生成对应文件。预期逻辑是通话开始(CALL_STARTED)时启动录音,通话结束(CALL_ENDED)时停止并保存文件。
核心录音组件代码
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:phone_state/phone_state.dart'; import 'package:record/record.dart'; import 'package:intl/intl.dart'; class RecordCall extends StatefulWidget { const RecordCall({super.key}); @override State<RecordCall> createState() => _RecordCallState(); } class _RecordCallState extends State<RecordCall> { bool granted = false; bool isRecording = false; late final AudioRecorder _audioRecorder; String? _audioPath; @override void initState() { try { _audioRecorder = AudioRecorder(); } catch (e) { debugPrint('Error initializing AudioRecorder: $e'); } super.initState(); PhoneState.stream.listen((PhoneState state) { debugPrint('Phone state: ${state.status}'); if (state.status == PhoneStateStatus.CALL_STARTED) { if (!isRecording) { debugPrint('Starting recording...'); startRecording(); } } else if (state.status == PhoneStateStatus.CALL_ENDED) { if (isRecording) { debugPrint('Stopping recording...'); stopRecording(); } } }); } Future<void> startRecording() async { try { final directory = await getExternalStorageDirectory(); if (directory == null) { debugPrint('Directory is null'); return; } final musicDirectory = Directory('${directory.path}/Music'); if (!musicDirectory.existsSync()) { musicDirectory.createSync(recursive: true); } String filePath = '${musicDirectory.path}/${_generateTimestamp()}.3gp'; await _audioRecorder.start( const RecordConfig(encoder: AudioEncoder.amrNb), path: filePath, ); } catch (e) { debugPrint('ERROR WHILE RECORDING: $e'); } } Future<void> stopRecording() async { try { final path = await _audioRecorder.stop(); if (path != null) { setState(() { isRecording = false; _audioPath = path; }); debugPrint('Recording stopped: $_audioPath'); } else { debugPrint('Recording stopped, but no file was created.'); } } catch (e) { debugPrint('Error while stopping recording: $e'); } } String _generateTimestamp() { return DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); } }
AndroidManifest权限配置
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_CALL_LOG" /> <uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
权限请求代码
main.dart
import 'package:voicenote/core/request_permission.dart'; Future<void> main() async{ WidgetsFlutterBinding.ensureInitialized(); bool permissionsGranted = await PermissionHandler.requestPermissions(); if (!permissionsGranted) { return; } await Supabase.initialize( url: SupabaseConfig.supabaseUrl, anonKey: SupabaseConfig.supabaseApiKey, ); ForegroundTaskService.init(); runApp(const VoiceNote()); }
request_permission.dart
import 'package:permission_handler/permission_handler.dart'; class PermissionHandler { static Future<bool> requestPermissions() async { var microphoneStatus = await Permission.microphone.request(); var phoneStatus = await Permission.phone.request(); var storageStatus = await Permission.storage.request(); return microphoneStatus.isGranted && phoneStatus.isGranted && storageStatus.isGranted; } }
问题排查与修复方案
1. 修正通话状态监听逻辑
phone_state的CALL_STARTED状态在呼入场景下,仅在用户接听通话后触发,但部分设备存在状态上报延迟或识别偏差。需结合isIncoming字段明确区分呼入/呼出场景,确保监听逻辑覆盖呼入接听事件:
PhoneState.stream.listen((PhoneState state) { debugPrint('Phone state: ${state.status}, isIncoming: ${state.isIncoming}'); // 呼入通话:用户接听后启动录音 if (state.status == PhoneStateStatus.CALL_STARTED && state.isIncoming) { if (!isRecording) { debugPrint('Starting incoming call recording...'); startRecording(); } } // 呼出通话:发起后启动录音 else if (state.status == PhoneStateStatus.CALL_STARTED && !state.isIncoming) { if (!isRecording) { debugPrint('Starting outgoing call recording...'); startRecording(); } } // 通话结束时停止录音 else if (state.status == PhoneStateStatus.CALL_ENDED) { if (isRecording) { debugPrint('Stopping recording...'); stopRecording(); } } });
2. 补全录音状态标记
当前startRecording方法未更新isRecording状态,可能导致重复触发或状态判断错误,需在录音启动成功后同步更新状态:
Future<void> startRecording() async { try { // ... 原路径处理代码 ... await _audioRecorder.start( const RecordConfig(encoder: AudioEncoder.amrNb), path: filePath, ); // 启动成功后更新状态 setState(() { isRecording = true; }); debugPrint('Recording started at: $filePath'); } catch (e) { debugPrint('ERROR WHILE RECORDING: $e'); } }
3. 完善Android权限与前台服务
- Android 12+ 通知权限:需添加
POST_NOTIFICATIONS权限,否则前台服务无法启动,后台录音会被系统限制:<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> - 启动前台服务:仅初始化前台服务不足以保持后台运行,需在通话开始时主动启动:
// 在startRecording方法开头添加 await ForegroundTaskService.start( notificationTitle: "通话录音中", notificationText: "正在录制通话内容", notificationIcon: "ic_notification", // 需在drawable目录添加对应图标 ); - Android 10+ 文件存储适配:
getExternalStorageDirectory()在Android 10及以上被限制,建议改用MediaStore保存文件:// 示例:通过MediaStore获取音乐目录路径 Future<String> getMusicFilePath() async { final timestamp = _generateTimestamp(); final contentValues = ContentValues({ MediaStore.MediaColumns.DISPLAY_NAME: '$timestamp.3gp', MediaStore.MediaColumns.MIME_TYPE: 'audio/3gpp', MediaStore.MediaColumns.RELATIVE_PATH: Environment.DIRECTORY_MUSIC, }); final uri = await MediaStore.Audio.Media.insert( context.contentResolver, contentValues, ); return uri.path!; }
4. 设备隐私权限配置
部分品牌设备(小米、华为等)有额外隐私限制,需手动在设备设置中:
- 允许应用获取通话状态权限
- 开启后台录音权限
- 解除应用的后台运行限制
内容的提问来源于stack exchange,提问作者ussrback
相关产品推荐
相关产品推荐

