From 467f1c1853ecf0f6c343bdd2eeff5184fc6eaebd Mon Sep 17 00:00:00 2001 From: Maurice Loskyll Date: Wed, 4 Dec 2024 12:44:03 +0100 Subject: [PATCH] feat(linux): Add startStream function for linux --- record_linux/lib/record_linux.dart | 104 +++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/record_linux/lib/record_linux.dart b/record_linux/lib/record_linux.dart index 76a5c037..01f156b4 100644 --- a/record_linux/lib/record_linux.dart +++ b/record_linux/lib/record_linux.dart @@ -17,6 +17,7 @@ class RecordLinux extends RecordPlatform { RecordState _state = RecordState.stop; String? _path; StreamController? _stateStreamCtrl; + Process? _fmediaStreamProcess; @override Future create(String recorderId) async {} @@ -129,6 +130,69 @@ class RecordLinux extends RecordPlatform { ); } + @override + Future> startStream( + String recorderId, RecordConfig config) async { + final supported = await isEncoderSupported(recorderId, config.encoder); + if (!supported) { + throw Exception('${config.encoder} is not supported.'); + } + + String numChannels; + if (config.numChannels == 6) { + numChannels = '5.1'; + } else if (config.numChannels == 8) { + numChannels = '7.1'; + } else if (config.numChannels == 1 || config.numChannels == 2) { + numChannels = config.numChannels.toString(); + } else { + throw Exception('${config.numChannels} config is not supported.'); + } + + final streamController = StreamController>(); + var fileExtension = ''; + + switch (config.encoder) { + case AudioEncoder.flac: + fileExtension = '.flac'; + break; + case AudioEncoder.opus: + fileExtension = '.opus'; + break; + case AudioEncoder.wav: + fileExtension = '.wav'; + break; + default: + throw Exception('${config.encoder} is not supported for streaming.'); + } + + await _callFMediaStream( + [ + '--notui', + '--globcmd.pipe-name=$_pipeProcName$recorderId', + '--record', + '--out=@stdout$fileExtension', + '--rate=${config.sampleRate}', + '--channels=$numChannels', + '--globcmd=listen', + '--gain=6.0', + if (config.device != null) '--dev-capture=${config.device!.id}', + ..._getEncoderSettings(config.encoder, config.bitRate), + ], + recorderId: recorderId, + outStreamCtrl: streamController, + onStarted: () { + _updateState(RecordState.record); + }, + ).catchError((error) { + print("Error in _callFMediaStream: $error"); + streamController.addError(error); + streamController.close(); + }); + + return streamController.stream.map((data) => Uint8List.fromList(data)); + } + @override Future stop(String recorderId) async { final path = _path; @@ -136,6 +200,9 @@ class RecordLinux extends RecordPlatform { await _callFMedia(['--globcmd=stop'], recorderId: recorderId); await _callFMedia(['--globcmd=quit'], recorderId: recorderId); + await _fmediaStreamProcess?.exitCode; + _fmediaStreamProcess = null; + _updateState(RecordState.stop); return path; @@ -251,6 +318,43 @@ class RecordLinux extends RecordPlatform { } } + Future _callFMediaStream( + List arguments, { + required String recorderId, + StreamController>? outStreamCtrl, + VoidCallback? onStarted, + bool consumeOutput = true, + }) async { + _fmediaStreamProcess = await Process.start(_fmediaBin, arguments); + + if (onStarted != null) { + onStarted(); + } + + // Listen to both stdout & stderr to not leak system resources. + if (consumeOutput) { + final out = outStreamCtrl ?? StreamController>(); + if (outStreamCtrl == null) out.stream.listen((event) {}); + final err = StreamController>(); + err.stream.listen((event) {}); + + _fmediaStreamProcess!.stdout.listen( + (data) { + if (!out.isClosed) { + out.add(data); + } + }, + onDone: () async { + if (outStreamCtrl != null && !outStreamCtrl.isClosed) { + outStreamCtrl.close(); + } else if (!out.isClosed) { + out.close(); + } + }, + ); + } + } + // Playback/Loopback: // device #1: FOO (High Definition Audio) - Default // Default Format: 2 channel, 48000 Hz