llfbandit / record

Audio recorder from microphone to a given file path. No external dependencies, MediaRecorder is used for Android an AVAudioRecorder for iOS.
https://pub.dev/packages/record
235 stars 200 forks source link

Unhandled Exception: LateInitializationError: Field '_stateStreamSubscription@1365055369' has not been initialized. #263

Closed osehmathias closed 10 months ago

osehmathias commented 10 months ago

Package version 5.0.2

Environment

Describe the bug

I am using Record in a PageView widget where you can swipe back and forth to perform different actions.

Record throws this error consistently.

Add your record configuration RecordConfig(...)

RecordConfig()

To Reproduce

Steps to reproduce the behavior:

  1. Use Record in a widget in a pageview
  2. Swipe to the end of the pageview
  3. Check the terminal
  4. See error

Expected behavior

No errors in the terminal. Record has no late initialised variables being accessed.

Additional context

This is the code:

import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:record/record.dart';

class Audio extends StatefulWidget {
  const Audio({Key? key}) : super(key: key);

  @override
  State<Audio> createState() => _AudioRecorderPageState();
}

class _AudioRecorderPageState extends State<Audio> {
  final AudioRecorder _audioRecorder = AudioRecorder();
  bool _isRecording = false;

  Future<void> _toggleRecording() async {
    if (_isRecording) {
      final path = await _audioRecorder.stop();
      debugPrint('Recording stopped, file saved at: $path');
      setState(() {
        _isRecording = false;
      });
    } else {
      final status = await Permission.microphone.request();

      if (status == PermissionStatus.granted) {
        final appDocDirectory = await getApplicationDocumentsDirectory();
        final filePath =
            '${appDocDirectory.path}/recording_${DateTime.now().millisecondsSinceEpoch}.m4a';
        await _audioRecorder.start(const RecordConfig(), path: filePath);
        setState(() {
          _isRecording = true;
        });
      } else if (status == PermissionStatus.permanentlyDenied) {
        _showPermissionDialog();
      }
    }
  }

  void _showPermissionDialog() {
    showDialog(
      context: context,
      builder: (BuildContext context) => AlertDialog(
        title: const Text('Microphone Permission Required'),
        content: const Text(
            'This app requires microphone access to record audio. Please enable microphone permission in app settings.'),
        actions: <Widget>[
          TextButton(
            child: const Text('Cancel'),
            onPressed: () => Navigator.of(context).pop(),
          ),
          TextButton(
            child: const Text('Open Settings'),
            onPressed: () {
              Navigator.of(context).pop();
              openAppSettings();
            },
          ),
        ],
      ),
    );
  }

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    if (_isRecording) {
      _audioRecorder.stop();
    }
    try {
      _audioRecorder.dispose();
    } catch (e) {
      debugPrint('e $e');
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        border: Border.all(color: Colors.grey[500]!, width: 1),
      ),
      child: Padding(
        padding: const EdgeInsets.all(1),
        child: Container(
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: Colors.grey[50],
          ),
          child: IconButton(
            iconSize: 100,
            icon: _isRecording
                ? Container(
                    decoration: const BoxDecoration(
                      shape: BoxShape.circle,
                      color: Colors.red,
                    ),
                    child: const Icon(Icons.stop, color: Colors.white),
                  )
                : Container(
                    decoration: const BoxDecoration(
                      shape: BoxShape.circle,
                      color: Colors.red,
                    ),
                    child: const Icon(Icons.mic, color: Colors.red),
                  ),
            onPressed: _toggleRecording,
          ),
        ),
      ),
    );
  }
}
llfbandit commented 10 months ago

Thanks for the report. Can you confirm it occurs when disposing AudioRecorder without calling any method before (start, stop, or anything else)?

llfbandit commented 10 months ago

Fixed in v5.0.3.

osehmathias commented 9 months ago

@llfbandit - thanks! Impressed with your package and speed of support.

However, the problem is still ongoing.

This is the error now:

[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: PlatformException(record, Recorder has not yet been created or has already been disposed., null, null)
#0      StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:651:7)
#1      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:322:18)
<asynchronous suspension>
#2      RecordMethodChannel.dispose (package:record_platform_interface/src/record_method_channel.dart:112:5)
<asynchronous suspension>
#3      AudioRecorder.dispose (package:record/src/record.dart:167:5)
<asynchronous suspension>
llfbandit commented 9 months ago

Thanks for your reactivity. v5.0.4 has been released. Better testing than assuming... 😞

osehmathias commented 9 months ago

Beautiful! This is working now! Thank you @llfbandit

Shoot me a link if you have a buy me a coffee or one of those things :)

llfbandit commented 9 months ago

I'll never provide such links ! (Also, I'm trying to avoid coffee now😄) Kind and supportive community is more than enough to me.

osehmathias commented 9 months ago

Legend

KeithBacalso commented 2 months ago

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart' as record_pkg;

import '../../../../utils/constants/app_colors.dart';
import '../../../../utils/constants/app_text_styles.dart';
import '../../../../utils/extensions/duration_extension.dart';
import '../../../../widgets/buttons/app_circular_button.dart';
import '../../cubits/recording/recording_cubit.dart';

class AudioRecorder extends StatefulWidget {
  const AudioRecorder({super.key});

  @override
  State<AudioRecorder> createState() => _AudioRecorderState();
}

class _AudioRecorderState extends State<AudioRecorder> {
  final _record = record_pkg.AudioRecorder();

  Duration _duration = Duration.zero;

  Timer? _timer;
  Timer? _volumeTimer;

  double _volume = 0.0;
  final double _minVolume = -45.0;

  bool _isRecording = false;

  @override
  void dispose() {
    _record.dispose();
    super.dispose();
  }

  @override
  void initState() {
    _record.onStateChanged().listen(
      (state) {
        if (!mounted) return;

        switch (state) {
          case record_pkg.RecordState.stop:
            _stopTimer();
            setState(() => _duration = Duration.zero);
          case record_pkg.RecordState.pause:
            _stopTimer();
            context
                .read<RecordingCubit>()
                .record(status: RecordingStatus.paused);
          case record_pkg.RecordState.record:
            _startTimer();
            context
                .read<RecordingCubit>()
                .record(status: RecordingStatus.played);
        }
      },
    );

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<RecordingCubit, RecordingState>(
      builder: (context, recordingState) {
        final status = recordingState.status;
        final isStatusInitializedOrRemoved =
            status == RecordingStatus.initialized ||
                status == RecordingStatus.removed;

        return isStatusInitializedOrRemoved
            ? _renderCircularRedStartRecordButton(context, status)
            : _renderRecordingTimeAndButtons(context, status);
      },
    );
  }

  void _updateVolume() async {
    final ampl = await _record.getAmplitude();

    if (ampl.current > _minVolume) {
      setState(() {
        _volume = (ampl.current - _minVolume) / _minVolume;
      });
      print('VOLUME: $_volume');
    }
  }

  int volume0to(int maxVolumeToDisplay) {
    // Invert the volume calculation to start from 100.
    return ((1 - _volume) * maxVolumeToDisplay).round().abs();
  }

  void _handleTrashRecording(BuildContext context) async {
    context.read<RecordingCubit>().record(status: RecordingStatus.removed);

    await _record.cancel();
    _stopTimer();
  }

  void _handlePlayOrPauseRecording(BuildContext context) async {
    if (_isRecording) {
      await _record.pause();
      _stopTimer();
      setState(() {
        _isRecording = false;
      });
      return;
    }

    await _record.resume();
    _startTimer();
    _isRecording = true;
  }

  void _handleFinishRecording(BuildContext context) async {
    final path = await _record.stop();

    if (!context.mounted) return;

    context
        .read<RecordingCubit>()
        .record(status: RecordingStatus.finished, path: path);
  }

  void _startTimer() {
    _timer?.cancel();
    _timer = Timer.periodic(
      const Duration(seconds: 1),
      (timer) => setState(() {
        _duration += const Duration(seconds: 1);
      }),
    );
    _volumeTimer ??= Timer.periodic(
        const Duration(milliseconds: 50), (timer) => _updateVolume());
  }

  void _stopTimer() {
    _timer?.cancel();
    _timer = null;
  }

  Column _renderCircularRedStartRecordButton(
    BuildContext context,
    RecordingStatus status,
  ) {
    return Column(
      children: [
        Text(
          _duration.formatAudioRecordTime,
          style: AppTextStyles.title1.copyWith(
            fontSize: 40,
          ),
        ),
        const SizedBox(height: 16),
        AppCircularButton(
          onPressed: () async {
            final appDocumentsDir = await getApplicationDocumentsDirectory();
            final filePath = p.join(appDocumentsDir.path, 'recording.m4a');
            if (await _record.hasPermission()) {
              await _record.start(
                const record_pkg.RecordConfig(),
                path: filePath,
              );
              _startTimer();
              setState(() {
                _isRecording = true;
              });
            }

            if (!context.mounted) return;

            context
                .read<RecordingCubit>()
                .record(status: RecordingStatus.played);
          },
          size: 95,
          child: const CircleAvatar(
            radius: 100,
            backgroundColor: AppColors.snackbarFailure,
          ),
        ),
      ],
    );
  }

  Column _renderRecordingTimeAndButtons(
    BuildContext context,
    RecordingStatus status,
  ) {
    return Column(
      children: [
        Text(
          _duration.formatAudioRecordTime,
          style: AppTextStyles.title1.copyWith(
            fontSize: 40,
          ),
        ),
        const SizedBox(height: 16),
        //* Uncomment to see the voice animation in action.
        Container(
          height: volume0to(200).toDouble(),
          width: volume0to(200).toDouble(),
          decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: AppColors.navixAccent.withOpacity(0.4)),
          child: Center(child: Text(volume0to(100).toString())),
        ),
        const SizedBox(height: 16),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            AppCircularButton(
              onPressed: () => _handleTrashRecording(context),
              size: 70,
              child: const Icon(
                Icons.delete_rounded,
                color: AppColors.snackbarFailure,
              ),
            ),
            AppCircularButton(
              onPressed: () => _handlePlayOrPauseRecording(context),
              size: 95,
              child: Icon(
                _isRecording ? Icons.pause_rounded : Icons.play_arrow_rounded,
                size: 50,
                color: AppColors.navixText,
              ),
            ),
            AppCircularButton(
              onPressed: () => _handleFinishRecording(context),
              size: 70,
              child: const Icon(
                Icons.check_rounded,
                color: AppColors.lightGreen,
              ),
            ),
          ],
        ),
      ],
    );
  }
}

having same error :

PlatformException (PlatformException(record, Recorder has not yet been created or has already been disposed., null, null))

If I go back to previous page.