import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:matrix/matrix.dart'; import 'package:path/path.dart' as path_lib; import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'events/audio_player.dart'; class RecordingViewModel extends StatefulWidget { final Widget Function(BuildContext, RecordingViewModelState) builder; const RecordingViewModel({ required this.builder, super.key, }); @override RecordingViewModelState createState() => RecordingViewModelState(); } class RecordingViewModelState extends State { Timer? _recorderSubscription; Duration duration = Duration.zero; bool error = false; bool isSending = false; bool get isRecording => _audioRecorder != null; AudioRecorder? _audioRecorder; final List amplitudeTimeline = []; String? fileName; bool isPaused = false; Future startRecording(Room room) async { room.client.getConfig(); // Preload server file configuration. if (PlatformInfos.isAndroid) { final info = await DeviceInfoPlugin().androidInfo; if (info.version.sdkInt < 19) { showOkAlertDialog( context: context, title: L10n.of(context).unsupportedAndroidVersion, message: L10n.of(context).unsupportedAndroidVersionLong, okLabel: L10n.of(context).close, ); return; } } if (await AudioRecorder().hasPermission() == false) return; final store = Matrix.of(context).store; final audioRecorder = _audioRecorder ??= AudioRecorder(); setState(() {}); try { final codec = kIsWeb // Web seems to create webm instead of ogg when using opus encoder // which does not play on iOS right now. So we use wav for now: ? AudioEncoder.wav // Everywhere else we use opus if supported by the platform: : await audioRecorder.isEncoderSupported(AudioEncoder.opus) ? AudioEncoder.opus : AudioEncoder.aacLc; fileName = 'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}'; String? path; if (!kIsWeb) { final tempDir = await getTemporaryDirectory(); path = path_lib.join(tempDir.path, fileName); } final result = await audioRecorder.hasPermission(); if (result != true) { setState(() => error = true); return; } await WakelockPlus.enable(); await audioRecorder.start( RecordConfig( bitRate: AppSettings.audioRecordingBitRate.getItem(store), sampleRate: AppSettings.audioRecordingSamplingRate.getItem(store), numChannels: AppSettings.audioRecordingNumChannels.getItem(store), autoGain: AppSettings.audioRecordingAutoGain.getItem(store), echoCancel: AppSettings.audioRecordingEchoCancel.getItem(store), noiseSuppress: AppSettings.audioRecordingNoiseSuppress.getItem(store), encoder: codec, ), path: path ?? '', ); setState(() => duration = Duration.zero); _subscribe(); } catch (_) { setState(() => error = true); rethrow; } } @override void dispose() { _reset(); super.dispose(); } void _subscribe() { _recorderSubscription?.cancel(); _recorderSubscription = Timer.periodic(const Duration(milliseconds: 100), (_) async { final amplitude = await _audioRecorder!.getAmplitude(); var value = 100 + amplitude.current * 2; value = value < 1 ? 1 : value; amplitudeTimeline.add(value); setState(() { duration += const Duration(milliseconds: 100); }); }); } void _reset() { WakelockPlus.disable(); _recorderSubscription?.cancel(); _audioRecorder?.stop(); _audioRecorder = null; isSending = false; error = false; fileName = null; duration = Duration.zero; amplitudeTimeline.clear(); isPaused = false; } void cancel() { setState(() { _reset(); }); } void pause() { _audioRecorder?.pause(); _recorderSubscription?.cancel(); setState(() { isPaused = true; }); } void resume() { _audioRecorder?.resume(); _subscribe(); setState(() { isPaused = false; }); } void stopAndSend( Future Function( String path, int duration, List waveform, String? fileName, ) onSend, ) async { _recorderSubscription?.cancel(); final path = await _audioRecorder?.stop(); if (path == null) throw ('Recording failed!'); const waveCount = AudioPlayerWidget.wavesCount; final step = amplitudeTimeline.length < waveCount ? 1 : (amplitudeTimeline.length / waveCount).round(); final waveform = []; for (var i = 0; i < amplitudeTimeline.length; i += step) { waveform.add((amplitudeTimeline[i] / 100 * 1024).round()); } setState(() { isSending = true; }); try { await onSend(path, duration.inMilliseconds, waveform, fileName); } catch (e, s) { Logs().e('Unable to send voice message', e, s); setState(() { isSending = false; }); return; } cancel(); } @override Widget build(BuildContext context) => widget.builder(context, this); } extension on AudioEncoder { String get fileExtension { switch (this) { case AudioEncoder.aacLc: case AudioEncoder.aacEld: case AudioEncoder.aacHe: return 'm4a'; case AudioEncoder.opus: return 'ogg'; case AudioEncoder.wav: return 'wav'; case AudioEncoder.amrNb: case AudioEncoder.amrWb: case AudioEncoder.flac: case AudioEncoder.pcm16bits: throw UnsupportedError('Not yet used'); } } }