You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 12:59:53