1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-10-22 22:43:22 +00:00

Start to record MP3 files

This commit is contained in:
Pierre HUBERT 2021-03-12 20:52:26 +01:00
parent 6fc1a263d2
commit e7b1beca50
7 changed files with 365 additions and 3 deletions

View File

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
/// Simple alert dialog
///
/// @author Pierre Hubert
Future<void> alert(BuildContext context, String msg) async {
await showDialog(context: context, builder: (c) => _AlertDialog(msg: msg));
}
class _AlertDialog extends StatelessWidget {
final String msg;
const _AlertDialog({Key key, @required this.msg})
: assert(msg != null),
super(key: key);
@override
Widget build(BuildContext context) {
return AlertDialog(
content: Text(msg),
actions: <Widget>[
MaterialButton(
child: Text("OK"),
onPressed: () => Navigator.of(context).pop(),
)
],
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:comunic/models/api_request.dart'; import 'package:comunic/models/api_request.dart';
import 'package:comunic/ui/dialogs/record_audio_dialog.dart';
import 'package:comunic/utils/files_utils.dart'; import 'package:comunic/utils/files_utils.dart';
import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart'; import 'package:comunic/utils/ui_utils.dart';
@ -17,6 +18,7 @@ enum _FileChoices {
TAKE_PICTURE, TAKE_PICTURE,
PICK_VIDEO, PICK_VIDEO,
TAKE_VIDEO, TAKE_VIDEO,
RECORD_AUDIO,
PICK_OTHER_FILE, PICK_OTHER_FILE,
} }
@ -65,12 +67,20 @@ List<_PickFileOption> get _optionsList => [
icon: Icons.videocam, icon: Icons.videocam,
canEnable: (l) => l.any(isVideo)), canEnable: (l) => l.any(isVideo)),
// Audio
_PickFileOption(
value: _FileChoices.RECORD_AUDIO,
label: tr("Record audio"),
icon: Icons.mic,
canEnable: (l) => l.any(isAudio)),
// Other // Other
_PickFileOption( _PickFileOption(
value: _FileChoices.PICK_OTHER_FILE, value: _FileChoices.PICK_OTHER_FILE,
label: tr("Browse files"), label: tr("Browse files"),
icon: Icons.folder_open, icon: Icons.folder_open,
canEnable: (l) => l.any((el) => !isImage(el) && !isVideo(el))), canEnable: (l) =>
l.any((el) => !isImage(el) && !isVideo(el) && !isAudio(el))),
]; ];
Future<BytesFile> showPickFileDialog({ Future<BytesFile> showPickFileDialog({
@ -138,6 +148,13 @@ Future<BytesFile> showPickFileDialog({
break; break;
// Record audio file
case _FileChoices.RECORD_AUDIO:
final bytes = await showRecordAudioDialog(context);
if (bytes == null) return null;
file = BytesFile("record.mp3", bytes);
break;
// Pick other files // Pick other files
case _FileChoices.PICK_OTHER_FILE: case _FileChoices.PICK_OTHER_FILE:
final pickedFile = await FilePicker.platform.pickFiles( final pickedFile = await FilePicker.platform.pickFiles(
@ -179,7 +196,7 @@ class _BottomSheetPickOption extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Container( Widget build(BuildContext context) => Container(
height: 300, height: 255,
child: ListView.builder( child: ListView.builder(
itemCount: options.length, itemCount: options.length,
itemBuilder: (c, i) => ListTile( itemBuilder: (c, i) => ListTile(

View File

@ -0,0 +1,298 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:comunic/ui/dialogs/alert_dialog.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/log_utils.dart';
import 'package:comunic/utils/ui_utils.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart';
import 'package:video_player/video_player.dart';
/// Record audio dialog
///
/// @author Pierre Hubert
/// Record audio
Future<Uint8List> showRecordAudioDialog(BuildContext context) async {
if (!await Record.hasPermission()) {
await alert(
context, "Permission d'accéder au périphérique audio refusée !");
return null;
}
final res = await showDialog(
context: context,
builder: (c) => Scaffold(
body: _RecordAudioDialog(),
backgroundColor: Colors.transparent,
),
barrierDismissible: false,
);
if (await Record.isRecording()) await Record.stop();
return res;
}
class _RecordAudioDialog extends StatefulWidget {
@override
__RecordAudioDialogState createState() => __RecordAudioDialogState();
}
class __RecordAudioDialogState extends State<_RecordAudioDialog> {
String _recordPath;
File get _recordFile => _recordPath == null ? null : File(_recordPath);
bool _recording = false;
bool get _hasRecord => !_recording && _recordPath != null;
VideoPlayerController _videoPlayerController;
bool _playing = false;
bool _paused = false;
/// Get record data. This getter can be accessed only once
Uint8List get _bytes {
final bytes = _recordFile.readAsBytesSync();
File(_recordPath).deleteSync();
return bytes;
}
@override
void dispose() {
_disposePlayer();
super.dispose();
}
void _disposePlayer() {
if (_videoPlayerController != null) {
_videoPlayerController.dispose();
}
_videoPlayerController = null;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(tr("Audio record")),
content: _buildContent(),
actions: <Widget>[
_ActionButton(
visible: !_recording,
text: tr("Cancel"),
onPressed: () => Navigator.of(context).pop(),
),
_ActionButton(
visible: _hasRecord,
text: tr("Send"),
onPressed: () => Navigator.of(context).pop(_bytes),
),
],
);
}
String get _status {
if (_recording)
return tr("Recording...");
else if (_paused)
return tr("Playback paused...");
else if (_playing)
return tr("Playing...");
else if (!_hasRecord)
return tr("Ready");
else
return tr("Done");
}
Widget _buildContent() => Row(
children: <Widget>[
Text(_status),
Spacer(),
// Start recording
_RecordAction(
visible: !_recording && !_playing,
icon: Icons.fiber_manual_record,
onTap: _startRecording,
color: Colors.red,
),
// Stop recording
_RecordAction(
visible: _recording,
icon: Icons.stop,
onTap: _stopRecording,
color: Colors.red,
),
// Play recording
_RecordAction(
visible: !_recording && _hasRecord && !_playing,
icon: Icons.play_arrow,
onTap: _playRecord,
),
// Pause playback
_RecordAction(
visible: _playing && !_paused,
icon: Icons.pause,
onTap: _pausePlayback,
),
// Resume recording
_RecordAction(
visible: _paused,
icon: Icons.play_arrow,
onTap: _resumePlayback,
),
// Stop recording
_RecordAction(
visible: _playing,
icon: Icons.stop,
onTap: _stopPlayback,
),
],
);
void _startRecording() async {
try {
if (_recordFile != null) _recordFile.deleteSync();
final dir = await getTemporaryDirectory();
_recordPath = path.join(dir.absolute.path, "tmp-audio-record.m4a");
await Record.start(path: _recordPath);
setState(() => _recording = true);
} catch (e, s) {
logError(e, s);
snack(context, tr("Error while recording!"));
}
}
void _stopRecording() async {
try {
await Record.stop();
setState(() => _recording = false);
} catch (e, s) {
logError(e, s);
snack(context, tr("Error while recording!"));
}
}
void _playRecord() async {
try {
_disposePlayer();
_videoPlayerController = VideoPlayerController.file(File(_recordPath));
await _videoPlayerController.initialize();
_videoPlayerController.addListener(() async {
if (_videoPlayerController == null) return;
if (_videoPlayerController.value.position ==
_videoPlayerController.value.duration) _stopPlayback();
});
await _videoPlayerController.play();
setState(() {
_playing = true;
_paused = false;
});
} catch (e, s) {
logError(e, s);
snack(context, tr("Error while playing record!"));
}
}
void _pausePlayback() async {
try {
await _videoPlayerController.pause();
setState(() => _paused = true);
} catch (e, s) {
logError(e, s);
snack(context, tr("Error while pausing playback!"));
}
}
void _resumePlayback() async {
try {
await _videoPlayerController.play();
setState(() => _paused = false);
} catch (e, s) {
logError(e, s);
snack(context, tr("Error while resuming playback!"));
}
}
void _stopPlayback() async {
try {
_disposePlayer();
setState(() {
_paused = false;
_playing = false;
});
} catch (e, s) {
logError(e, s);
snack(context, tr("Error while stopping playback!"));
}
}
}
class _RecordAction extends StatelessWidget {
final bool visible;
final IconData icon;
final void Function() onTap;
final Color color;
const _RecordAction({
Key key,
@required this.visible,
@required this.icon,
@required this.onTap,
this.color,
}) : assert(visible != null),
assert(icon != null),
super(key: key);
@override
Widget build(BuildContext context) {
if (!visible) return Container(width: 0, height: 0);
return IconButton(icon: Icon(icon, color: color), onPressed: onTap);
}
}
class _ActionButton extends StatelessWidget {
final bool visible;
final String text;
final void Function() onPressed;
const _ActionButton({
Key key,
this.visible,
this.text,
this.onPressed,
}) : assert(visible != null),
assert(text != null),
super(key: key);
@override
Widget build(BuildContext context) {
if (!visible) return Container();
return MaterialButton(
onPressed: onPressed,
child: Text(text.toUpperCase()),
);
}
}

View File

@ -49,5 +49,8 @@ Future<PickedFile> pickImage(BuildContext context) async {
/// Check if a mime type maps to an image or not /// Check if a mime type maps to an image or not
bool isImage(String mimeType) => mimeType.startsWith("image/"); bool isImage(String mimeType) => mimeType.startsWith("image/");
/// Check if a mime type maps to an image or not /// Check if a mime type maps to a video or not
bool isVideo(String mimeType) => mimeType.startsWith("video/mp4"); bool isVideo(String mimeType) => mimeType.startsWith("video/mp4");
/// Check if a mime type maps to an audio file or not
bool isAudio(String mimeType) => mimeType.startsWith("audio/mpeg");

View File

@ -83,6 +83,10 @@ void showSimpleSnack(BuildContext context, String message) {
Scaffold.of(context).showSnackBar(SnackBar(content: Text(message))); Scaffold.of(context).showSnackBar(SnackBar(content: Text(message)));
} }
void snack(BuildContext context, String message) {
Scaffold.of(context).showSnackBar(SnackBar(content: Text(message)));
}
/// Show an alert dialog to ask the user to enter a string /// Show an alert dialog to ask the user to enter a string
/// ///
/// Returns entered string if the dialog is confirmed, null else /// Returns entered string if the dialog is confirmed, null else

View File

@ -464,6 +464,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
record:
dependency: "direct main"
description:
name: record
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:

View File

@ -112,6 +112,9 @@ dependencies:
# Create video thumbnails # Create video thumbnails
video_thumbnail: ^0.2.5+1 video_thumbnail: ^0.2.5+1
# Record audio file
record: ^1.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter