Skip to content

After resuming recording, toStream will no longer be called back when there is audio data. #1211

@liyofx

Description

@liyofx

When recording is paused and then resumed using toStream, toStream will no longer be called back when there is audio data.

flutter_sound: ^9.30.0
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:permission_handler/permission_handler.dart';

/*

This is a very simple example for Flutter Sound beginners,
hat shows how to record, and then playback a file.

This example is really basic.

 */

typedef _Fn = void Function();

enum RecordMethod { toFile, toStream }

/* This does not work. on Android we must have the Manifest.permission.CAPTURE_AUDIO_OUTPUT permission.
 * But this permission is _is reserved for use by system components and is not available to third-party applications._
 * Pleaser look to [this](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_UPLINK)
 *
 * I think that the problem is because it is illegal to record a communication in many countries.
 * Probably this stands also on iOS.
 * Actually I am unable to record DOWNLINK on my Xiaomi Chinese phone.
 *
 */
//const theSource = AudioSource.voiceUpLink;
//const theSource = AudioSource.voiceDownlink;

const theSource = AudioSource.microphone;

// Example app.
class SimpleRecorder extends StatefulWidget {
  const SimpleRecorder({super.key});

  @override
  State<SimpleRecorder> createState() => _SimpleRecorderState();
}

class _SimpleRecorderState extends State<SimpleRecorder> {
  @override
  void initState() {
    _mPlayer!.openPlayer().then((value) {
      setState(() {
        _mPlayerIsInited = true;
      });
    });

    openTheRecorder().then((value) {
      setState(() {
        _mRecorderIsInited = true;
      });
    });
    super.initState();
  }

  @override
  void dispose() {
    _subscription?.cancel();
    _subscription = null;
    _mPlayer!.closePlayer();
    _mPlayer = null;

    _mRecorder!.closeRecorder();
    _mRecorder = null;
    super.dispose();
  }

  // ------------------------------ This is the recorder stuff -----------------------------

  Codec _codec = Codec.aacMP4;
  String _mPath = 'tau_file.mp4';
  bool _mRecorderIsInited = false;

  /// Our player
  FlutterSoundPlayer? _mPlayer = FlutterSoundPlayer();

  /// Our recorder
  FlutterSoundRecorder? _mRecorder = FlutterSoundRecorder();

  /// Request permission to record something and open the recorder
  Future<void> openTheRecorder() async {
    if (!kIsWeb) {
      var status = await Permission.microphone.request();
      if (status != PermissionStatus.granted) {
        throw RecordingPermissionException('Microphone permission not granted');
      }
    }
    await _mRecorder!.openRecorder();
    if (!await _mRecorder!.isEncoderSupported(_codec) && kIsWeb) {
      _codec = Codec.opusWebM;
      _mPath = 'tau_file.webm';
      if (!await _mRecorder!.isEncoderSupported(_codec) && kIsWeb) {
        _mRecorderIsInited = true;
        return;
      }
    }
    _mRecorderIsInited = true;
  }

  final List<int> tempBuffer = [];

  /// Begin to record.
  /// This is our main function.
  /// We ask Flutter Sound to record to a File.
  void record() {
    if (_method == RecordMethod.toFile) {
      _mRecorder!.startRecorder(toFile: _mPath, codec: _codec, audioSource: theSource).then((value) {
        setState(() {});
      });
    } else {
      var toStream = StreamController<Uint8List>();
      _subscription = toStream.stream.listen((buffer) {
        // buffer: Uint8List → PCM 数据
        print("收到 PCM 数据: ${buffer.length} 字节");
        tempBuffer.addAll(buffer);
      });
      _mRecorder!.startRecorder(toStream: toStream.sink, codec: Codec.pcm16, audioSource: theSource).then((value) {
        setState(() {});
      });
    }
  }

  /// Stop the recorder
  void stopRecorder() async {
    await _mRecorder!.stopRecorder().then((value) {
      setState(() {
        //var url = value;
        _mplaybackReady = true;
      });
    });
  }

  void pauseRecorder() {
    _mRecorder!.pauseRecorder().then((value) {
      setState(() {});
    });
  }

  void resumeRecorder() {
    _mRecorder!.resumeRecorder().then((value) {
      setState(() {});
    });
  }

  // ----------------------------- This is the player stuff ---------------------------------

  bool _mplaybackReady = false;
  bool _mPlayerIsInited = false;

  /// Begin to play the recorded sound
  void play() {
    assert(_mPlayerIsInited && _mplaybackReady && _mRecorder!.isStopped && _mPlayer!.isStopped);
    if (_method == RecordMethod.toFile) {
      _mPlayer!
          .startPlayer(
            fromURI: _mPath,
            whenFinished: () {
              setState(() {});
            },
          )
          .then((value) {
            setState(() {});
          });
    } else {
      _mPlayer!
          .startPlayer(
            fromDataBuffer: Uint8List.fromList(tempBuffer),
            codec: Codec.pcm16,
            whenFinished: () {
              setState(() {});
            },
          )
          .then((value) {
            setState(() {});
          });
    }
  }

  /// Stop the player
  void stopPlayer() {
    _mPlayer!.stopPlayer().then((value) {
      setState(() {});
    });
  }

  // ----------------------------- UI --------------------------------------------

  _Fn? getRecorderFn() {
    if (!_mRecorderIsInited || !_mPlayer!.isStopped) {
      return null;
    }
    return _mRecorder!.isStopped ? record : stopRecorder;
  }

  _Fn? getPauseFn() {
    if (!_mRecorderIsInited || !_mPlayer!.isStopped) {
      return null;
    }
    return _mRecorder!.isPaused ? resumeRecorder : pauseRecorder;
  }

  _Fn? getPlaybackFn() {
    if (!_mPlayerIsInited || !_mplaybackReady || !_mRecorder!.isStopped) {
      return null;
    }
    return _mPlayer!.isStopped ? play : stopPlayer;
  }

  var _method = RecordMethod.toFile;

  StreamSubscription<Uint8List>? _subscription;

  _onChanged(RecordMethod? value) {
    return () {
      setState(() {
        _method = value ?? RecordMethod.toFile;
      });
    };
  }

  @override
  Widget build(BuildContext context) {
    Widget makeBody() {
      return Column(
        children: [
          Container(
            margin: const EdgeInsets.all(3),
            padding: const EdgeInsets.all(3),
            decoration: BoxDecoration(color: const Color(0xFFFAF0E6), border: Border.all(color: Colors.indigo, width: 3)),
            child: Row(
              spacing: 20,
              children: [
                GestureDetector(
                  onTap: _onChanged(RecordMethod.toFile),
                  child: Padding(padding: const EdgeInsets.all(8.0), child: Row(children: [Icon(_method == RecordMethod.toFile ? Icons.radio_button_on : Icons.radio_button_off), Text("toFile")])),
                ), //
                GestureDetector(
                  onTap: _onChanged(RecordMethod.toStream),
                  child: Padding(padding: const EdgeInsets.all(8.0), child: Row(children: [Icon(_method == RecordMethod.toStream ? Icons.radio_button_on : Icons.radio_button_off), Text("toStream")])),
                ), //
              ],
            ),
          ),

          Container(
            margin: const EdgeInsets.all(3),
            padding: const EdgeInsets.all(3),
            height: 80,
            width: double.infinity,
            alignment: Alignment.center,
            decoration: BoxDecoration(color: const Color(0xFFFAF0E6), border: Border.all(color: Colors.indigo, width: 3)),
            child: Row(
              spacing: 12,
              children: [
                if (!_mRecorder!.isStopped) ElevatedButton(onPressed: getPauseFn(), child: Text(_mRecorder!.isPaused ? 'Resume' : 'Pause')),
                ElevatedButton(onPressed: getRecorderFn(), child: Text(_mRecorder!.isRecording ? 'Stop' : 'Record')),
                Text(_mRecorder!.isRecording ? 'Recording in progress' : 'Recorder is stopped'),
              ],
            ),
          ),
          Container(
            margin: const EdgeInsets.all(3),
            padding: const EdgeInsets.all(3),
            height: 80,
            width: double.infinity,
            alignment: Alignment.center,
            decoration: BoxDecoration(color: const Color(0xFFFAF0E6), border: Border.all(color: Colors.indigo, width: 3)),
            child: Row(children: [ElevatedButton(onPressed: getPlaybackFn(), child: Text(_mPlayer!.isPlaying ? 'Stop' : 'Play')), const SizedBox(width: 20), Text(_mPlayer!.isPlaying ? 'Playback in progress' : 'Player is stopped')]),
          ),
        ],
      );
    }

    return Scaffold(backgroundColor: Colors.blue, appBar: AppBar(title: const Text('Simple Recorder')), body: makeBody());
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions