flet-audio-recorder 0.85.0.dev2__tar.gz → 0.85.0.dev4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flet_audio_recorder-0.85.0.dev2/src/flet_audio_recorder.egg-info → flet_audio_recorder-0.85.0.dev4}/PKG-INFO +3 -3
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/README.md +1 -1
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/pyproject.toml +2 -2
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/src/flet_audio_recorder/__init__.py +6 -0
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/src/flet_audio_recorder/audio_recorder.py +55 -17
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/src/flet_audio_recorder/types.py +85 -1
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4/src/flet_audio_recorder.egg-info}/PKG-INFO +3 -3
- flet_audio_recorder-0.85.0.dev4/src/flet_audio_recorder.egg-info/requires.txt +1 -0
- flet_audio_recorder-0.85.0.dev4/src/flutter/flet_audio_recorder/lib/src/audio_recorder.dart +470 -0
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/src/flutter/flet_audio_recorder/pubspec.yaml +1 -0
- flet_audio_recorder-0.85.0.dev2/src/flet_audio_recorder.egg-info/requires.txt +0 -1
- flet_audio_recorder-0.85.0.dev2/src/flutter/flet_audio_recorder/lib/src/audio_recorder.dart +0 -91
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/LICENSE +0 -0
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/setup.cfg +0 -0
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/src/flet_audio_recorder.egg-info/SOURCES.txt +0 -0
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/src/flet_audio_recorder.egg-info/dependency_links.txt +0 -0
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/src/flet_audio_recorder.egg-info/top_level.txt +0 -0
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/src/flutter/flet_audio_recorder/lib/flet_audio_recorder.dart +0 -0
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/src/flutter/flet_audio_recorder/lib/src/extension.dart +0 -0
- {flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/src/flutter/flet_audio_recorder/lib/src/utils/audio_recorder.dart +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flet-audio-recorder
|
|
3
|
-
Version: 0.85.0.
|
|
3
|
+
Version: 0.85.0.dev4
|
|
4
4
|
Summary: Adds audio recording support to Flet apps.
|
|
5
5
|
Author-email: Flet contributors <hello@flet.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -11,7 +11,7 @@ Project-URL: Issues, https://github.com/flet-dev/flet/issues
|
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: flet==0.85.0.
|
|
14
|
+
Requires-Dist: flet==0.85.0.dev4
|
|
15
15
|
Dynamic: license-file
|
|
16
16
|
|
|
17
17
|
# flet-audio-recorder
|
|
@@ -59,4 +59,4 @@ To install the `flet-audio-recorder` package and add it to your project dependen
|
|
|
59
59
|
|
|
60
60
|
### Examples
|
|
61
61
|
|
|
62
|
-
For examples, see [these](https://github.com/flet-dev/flet/tree/main/sdk/python/examples/
|
|
62
|
+
For examples, see [these](https://github.com/flet-dev/flet/tree/main/sdk/python/examples/extensions/audio_recorder).
|
|
@@ -43,4 +43,4 @@ To install the `flet-audio-recorder` package and add it to your project dependen
|
|
|
43
43
|
|
|
44
44
|
### Examples
|
|
45
45
|
|
|
46
|
-
For examples, see [these](https://github.com/flet-dev/flet/tree/main/sdk/python/examples/
|
|
46
|
+
For examples, see [these](https://github.com/flet-dev/flet/tree/main/sdk/python/examples/extensions/audio_recorder).
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flet-audio-recorder"
|
|
3
|
-
version = "0.85.0.
|
|
3
|
+
version = "0.85.0.dev4"
|
|
4
4
|
description = "Adds audio recording support to Flet apps."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Flet contributors", email = "hello@flet.dev" }]
|
|
7
7
|
license = "Apache-2.0"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"flet==0.85.0.
|
|
10
|
+
"flet==0.85.0.dev4",
|
|
11
11
|
]
|
|
12
12
|
|
|
13
13
|
[project.urls]
|
|
@@ -6,6 +6,9 @@ from .types import (
|
|
|
6
6
|
AudioRecorderConfiguration,
|
|
7
7
|
AudioRecorderState,
|
|
8
8
|
AudioRecorderStateChangeEvent,
|
|
9
|
+
AudioRecorderStreamEvent,
|
|
10
|
+
AudioRecorderUploadEvent,
|
|
11
|
+
AudioRecorderUploadSettings,
|
|
9
12
|
InputDevice,
|
|
10
13
|
IosAudioCategoryOption,
|
|
11
14
|
IosRecorderConfiguration,
|
|
@@ -19,6 +22,9 @@ __all__ = [
|
|
|
19
22
|
"AudioRecorderConfiguration",
|
|
20
23
|
"AudioRecorderState",
|
|
21
24
|
"AudioRecorderStateChangeEvent",
|
|
25
|
+
"AudioRecorderStreamEvent",
|
|
26
|
+
"AudioRecorderUploadEvent",
|
|
27
|
+
"AudioRecorderUploadSettings",
|
|
22
28
|
"InputDevice",
|
|
23
29
|
"IosAudioCategoryOption",
|
|
24
30
|
"IosRecorderConfiguration",
|
|
@@ -7,6 +7,9 @@ from .types import (
|
|
|
7
7
|
AudioEncoder,
|
|
8
8
|
AudioRecorderConfiguration,
|
|
9
9
|
AudioRecorderStateChangeEvent,
|
|
10
|
+
AudioRecorderStreamEvent,
|
|
11
|
+
AudioRecorderUploadEvent,
|
|
12
|
+
AudioRecorderUploadSettings,
|
|
10
13
|
InputDevice,
|
|
11
14
|
)
|
|
12
15
|
|
|
@@ -18,9 +21,8 @@ class AudioRecorder(ft.Service):
|
|
|
18
21
|
"""
|
|
19
22
|
A control that allows you to record audio from your device.
|
|
20
23
|
|
|
21
|
-
This control can record audio using different
|
|
22
|
-
audio
|
|
23
|
-
of various audio recording parameters such as
|
|
24
|
+
This control can record audio using different audio encoders and also allows
|
|
25
|
+
configuration of various audio recording parameters such as
|
|
24
26
|
noise suppression, echo cancellation, and more.
|
|
25
27
|
"""
|
|
26
28
|
|
|
@@ -33,40 +35,74 @@ class AudioRecorder(ft.Service):
|
|
|
33
35
|
|
|
34
36
|
on_state_change: Optional[ft.EventHandler[AudioRecorderStateChangeEvent]] = None
|
|
35
37
|
"""
|
|
36
|
-
|
|
38
|
+
Called when recording state changes.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
on_upload: Optional[ft.EventHandler[AudioRecorderUploadEvent]] = None
|
|
42
|
+
"""
|
|
43
|
+
Called when streaming upload progress or errors are available.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
on_stream: Optional[ft.EventHandler[AudioRecorderStreamEvent]] = None
|
|
47
|
+
"""
|
|
48
|
+
Called when a raw :attr:`~flet_audio_recorder.AudioEncoder.PCM16BITS` \
|
|
49
|
+
recording chunk is available.
|
|
37
50
|
"""
|
|
38
51
|
|
|
39
52
|
async def start_recording(
|
|
40
53
|
self,
|
|
41
54
|
output_path: Optional[str] = None,
|
|
42
55
|
configuration: Optional[AudioRecorderConfiguration] = None,
|
|
56
|
+
upload: Optional[AudioRecorderUploadSettings] = None,
|
|
43
57
|
) -> bool:
|
|
44
58
|
"""
|
|
45
|
-
Starts recording audio and saves it to
|
|
59
|
+
Starts recording audio and saves it to a file or streams it.
|
|
60
|
+
|
|
61
|
+
If neither `upload` nor :attr:`on_stream` is used, `output_path` must be
|
|
62
|
+
provided on platforms other than web.
|
|
46
63
|
|
|
47
|
-
|
|
64
|
+
When streaming, use :attr:`~flet_audio_recorder.AudioEncoder.PCM16BITS` as
|
|
65
|
+
the encoder. In that case, emitted or uploaded
|
|
66
|
+
:attr:`~flet_audio_recorder.AudioRecorderStreamEvent.chunk`s contain raw PCM16
|
|
67
|
+
data. In some use cases, these chunks can be wrapped in a container such as
|
|
68
|
+
WAV if the output must be directly playable as an audio file.
|
|
48
69
|
|
|
49
70
|
Args:
|
|
50
71
|
output_path: The file path where the audio will be saved.
|
|
51
72
|
It must be specified if not on web.
|
|
52
|
-
configuration: The configuration for the audio recorder.
|
|
53
|
-
|
|
73
|
+
configuration: The configuration for the audio recorder. If `None`, the
|
|
74
|
+
:attr:`flet_audio_recorder.AudioRecorder.configuration` will be used.
|
|
75
|
+
upload: Upload settings to stream recording bytes directly
|
|
76
|
+
to a destination, for example a URL returned by
|
|
77
|
+
:meth:`flet.Page.get_upload_url`.
|
|
54
78
|
|
|
55
79
|
Returns:
|
|
56
80
|
`True` if recording was successfully started, `False` otherwise.
|
|
57
81
|
|
|
58
82
|
Raises:
|
|
59
|
-
ValueError: If `output_path` is not provided on platforms other than web
|
|
83
|
+
ValueError: If `output_path` is not provided on platforms other than web
|
|
84
|
+
when neither streaming nor uploads are requested.
|
|
85
|
+
ValueError: If streaming is requested with an encoder other than
|
|
86
|
+
:attr:`~flet_audio_recorder.AudioEncoder.PCM16BITS`.
|
|
60
87
|
"""
|
|
61
|
-
|
|
88
|
+
is_streaming = upload is not None or self.on_stream is not None
|
|
89
|
+
if not is_streaming and not (self.page.web or output_path):
|
|
62
90
|
raise ValueError("output_path must be provided on platforms other than web")
|
|
91
|
+
|
|
92
|
+
effective_configuration = (
|
|
93
|
+
configuration if configuration is not None else self.configuration
|
|
94
|
+
)
|
|
95
|
+
if is_streaming and effective_configuration.encoder != AudioEncoder.PCM16BITS:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"Streaming recordings require AudioEncoder.PCM16BITS as encoder."
|
|
98
|
+
)
|
|
99
|
+
|
|
63
100
|
return await self._invoke_method(
|
|
64
101
|
method_name="start_recording",
|
|
65
102
|
arguments={
|
|
66
103
|
"output_path": output_path,
|
|
67
|
-
"configuration":
|
|
68
|
-
|
|
69
|
-
else self.configuration,
|
|
104
|
+
"configuration": effective_configuration,
|
|
105
|
+
"upload": upload,
|
|
70
106
|
},
|
|
71
107
|
)
|
|
72
108
|
|
|
@@ -81,10 +117,11 @@ class AudioRecorder(ft.Service):
|
|
|
81
117
|
|
|
82
118
|
async def stop_recording(self) -> Optional[str]:
|
|
83
119
|
"""
|
|
84
|
-
Stops the audio recording and optionally returns the
|
|
120
|
+
Stops the audio recording and optionally returns the recording location.
|
|
85
121
|
|
|
86
122
|
Returns:
|
|
87
|
-
The file path where the audio was saved
|
|
123
|
+
The local file path where the audio was saved, a Blob URL on web, or
|
|
124
|
+
`None` when streaming (i.e. when `upload` or :attr:`on_stream` is set).
|
|
88
125
|
"""
|
|
89
126
|
return await self._invoke_method("stop_recording")
|
|
90
127
|
|
|
@@ -141,9 +178,10 @@ class AudioRecorder(ft.Service):
|
|
|
141
178
|
|
|
142
179
|
async def has_permission(self) -> bool:
|
|
143
180
|
"""
|
|
144
|
-
Checks if the app has permission to record audio.
|
|
181
|
+
Checks if the app has permission to record audio, requesting it if needed.
|
|
145
182
|
|
|
146
183
|
Returns:
|
|
147
|
-
`True` if
|
|
184
|
+
`True` if permission is already granted or granted after the request;
|
|
185
|
+
`False` otherwise.
|
|
148
186
|
"""
|
|
149
187
|
return await self._invoke_method("has_permission")
|
{flet_audio_recorder-0.85.0.dev2 → flet_audio_recorder-0.85.0.dev4}/src/flet_audio_recorder/types.py
RENAMED
|
@@ -14,6 +14,9 @@ __all__ = [
|
|
|
14
14
|
"AudioRecorderConfiguration",
|
|
15
15
|
"AudioRecorderState",
|
|
16
16
|
"AudioRecorderStateChangeEvent",
|
|
17
|
+
"AudioRecorderStreamEvent",
|
|
18
|
+
"AudioRecorderUploadEvent",
|
|
19
|
+
"AudioRecorderUploadSettings",
|
|
17
20
|
"InputDevice",
|
|
18
21
|
"IosAudioCategoryOption",
|
|
19
22
|
"IosRecorderConfiguration",
|
|
@@ -38,13 +41,62 @@ class AudioRecorderStateChangeEvent(ft.Event["AudioRecorder"]):
|
|
|
38
41
|
"""
|
|
39
42
|
Event payload for recorder state transitions.
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
Delivered by :attr:`flet_audio_recorder.AudioRecorder.on_state_change`.
|
|
42
45
|
"""
|
|
43
46
|
|
|
44
47
|
state: AudioRecorderState
|
|
45
48
|
"""The new state of the audio recorder."""
|
|
46
49
|
|
|
47
50
|
|
|
51
|
+
@dataclass
|
|
52
|
+
class AudioRecorderUploadEvent(ft.Event["AudioRecorder"]):
|
|
53
|
+
"""
|
|
54
|
+
Event payload for streaming recording uploads.
|
|
55
|
+
|
|
56
|
+
Delivered by :attr:`flet_audio_recorder.AudioRecorder.on_upload` for
|
|
57
|
+
uploads started with :meth:`flet_audio_recorder.AudioRecorder.start_recording`.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
file_name: Optional[str] = None
|
|
61
|
+
"""Name provided by :attr:`AudioRecorderUploadSettings.file_name`."""
|
|
62
|
+
|
|
63
|
+
progress: Optional[float] = None
|
|
64
|
+
"""
|
|
65
|
+
Upload progress from `0.0` to `1.0`.
|
|
66
|
+
|
|
67
|
+
Streaming uploads do not know their total size until recording stops, so
|
|
68
|
+
:attr:`bytes_uploaded` is usually the best progress indicator while recording is
|
|
69
|
+
active.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
bytes_uploaded: Optional[int] = None
|
|
73
|
+
"""Number of bytes uploaded so far."""
|
|
74
|
+
|
|
75
|
+
error: Optional[str] = None
|
|
76
|
+
"""Error message if the upload failed."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class AudioRecorderStreamEvent(ft.Event["AudioRecorder"]):
|
|
81
|
+
"""
|
|
82
|
+
Event payload for raw recording stream chunks.
|
|
83
|
+
|
|
84
|
+
Delivered by :attr:`flet_audio_recorder.AudioRecorder.on_stream`.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
chunk: bytes
|
|
88
|
+
"""
|
|
89
|
+
Raw :attr:`~flet_audio_recorder.AudioEncoder.PCM16BITS` audio bytes emitted by \
|
|
90
|
+
:class:`~flet_audio_recorder.AudioRecorder`.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
sequence: int
|
|
94
|
+
"""Incremental chunk number."""
|
|
95
|
+
|
|
96
|
+
bytes_streamed: int
|
|
97
|
+
"""Total number of bytes delivered through :attr:`chunk` so far."""
|
|
98
|
+
|
|
99
|
+
|
|
48
100
|
class AudioEncoder(Enum):
|
|
49
101
|
"""
|
|
50
102
|
Represents the different audio encoders for audio recording.
|
|
@@ -357,3 +409,35 @@ class AudioRecorderConfiguration:
|
|
|
357
409
|
"""
|
|
358
410
|
iOS specific configuration.
|
|
359
411
|
"""
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@ft.value
|
|
415
|
+
class AudioRecorderUploadSettings:
|
|
416
|
+
"""
|
|
417
|
+
Upload settings for streaming recordings.
|
|
418
|
+
|
|
419
|
+
Note:
|
|
420
|
+
Uploads started by :meth:`flet_audio_recorder.AudioRecorder.start_recording`
|
|
421
|
+
send raw :attr:`~flet_audio_recorder.AudioEncoder.PCM16BITS` bytes. They do
|
|
422
|
+
not add a playable audio container such as WAV.
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
upload_url: str
|
|
426
|
+
"""
|
|
427
|
+
Destination URL, for example one returned by :meth:`flet.Page.get_upload_url`.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
method: str = "PUT"
|
|
431
|
+
"""
|
|
432
|
+
HTTP method to use when uploading the streamed bytes.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
headers: Optional[dict[str, str]] = None
|
|
436
|
+
"""
|
|
437
|
+
HTTP headers sent with the upload request.
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
file_name: Optional[str] = None
|
|
441
|
+
"""
|
|
442
|
+
Friendly name reported in upload events.
|
|
443
|
+
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flet-audio-recorder
|
|
3
|
-
Version: 0.85.0.
|
|
3
|
+
Version: 0.85.0.dev4
|
|
4
4
|
Summary: Adds audio recording support to Flet apps.
|
|
5
5
|
Author-email: Flet contributors <hello@flet.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -11,7 +11,7 @@ Project-URL: Issues, https://github.com/flet-dev/flet/issues
|
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: flet==0.85.0.
|
|
14
|
+
Requires-Dist: flet==0.85.0.dev4
|
|
15
15
|
Dynamic: license-file
|
|
16
16
|
|
|
17
17
|
# flet-audio-recorder
|
|
@@ -59,4 +59,4 @@ To install the `flet-audio-recorder` package and add it to your project dependen
|
|
|
59
59
|
|
|
60
60
|
### Examples
|
|
61
61
|
|
|
62
|
-
For examples, see [these](https://github.com/flet-dev/flet/tree/main/sdk/python/examples/
|
|
62
|
+
For examples, see [these](https://github.com/flet-dev/flet/tree/main/sdk/python/examples/extensions/audio_recorder).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flet==0.85.0.dev4
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'dart:typed_data';
|
|
3
|
+
|
|
4
|
+
import 'package:flet/flet.dart';
|
|
5
|
+
import 'package:flutter/widgets.dart';
|
|
6
|
+
import 'package:http/http.dart' as http;
|
|
7
|
+
import 'package:record/record.dart';
|
|
8
|
+
|
|
9
|
+
import 'utils/audio_recorder.dart';
|
|
10
|
+
|
|
11
|
+
class AudioRecorderService extends FletService {
|
|
12
|
+
AudioRecorderService({required super.control});
|
|
13
|
+
|
|
14
|
+
AudioRecorder? recorder;
|
|
15
|
+
StreamSubscription<RecordState>? _onStateChangedSubscription;
|
|
16
|
+
|
|
17
|
+
// Subscription to the raw PCM audio stream produced by `recorder.startStream`.
|
|
18
|
+
// Only active while a streaming recording (upload and/or Python-side stream)
|
|
19
|
+
// is in progress; `null` for regular file-based recordings.
|
|
20
|
+
StreamSubscription<Uint8List>? _recordStreamSubscription;
|
|
21
|
+
|
|
22
|
+
// Holds the state of the current streaming recording (upload request,
|
|
23
|
+
// bytes counter, completion signal). `null` when no streaming is active.
|
|
24
|
+
_StreamingSession? _streamSession;
|
|
25
|
+
|
|
26
|
+
@override
|
|
27
|
+
void init() {
|
|
28
|
+
super.init();
|
|
29
|
+
debugPrint("AudioRecorder.init($hashCode)");
|
|
30
|
+
control.addInvokeMethodListener(_invokeMethod);
|
|
31
|
+
|
|
32
|
+
recorder = AudioRecorder();
|
|
33
|
+
|
|
34
|
+
_onStateChangedSubscription = recorder!.onStateChanged().listen((state) {
|
|
35
|
+
_onStateChanged.call(state);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
void _onStateChanged(RecordState state) {
|
|
40
|
+
var stateMap = {
|
|
41
|
+
RecordState.record: "recording",
|
|
42
|
+
RecordState.pause: "paused",
|
|
43
|
+
RecordState.stop: "stopped",
|
|
44
|
+
};
|
|
45
|
+
control.triggerEvent("state_change", stateMap[state]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Future<dynamic> _invokeMethod(String name, dynamic args) async {
|
|
49
|
+
debugPrint("AudioRecorder.$name($args)");
|
|
50
|
+
switch (name) {
|
|
51
|
+
case "start_recording":
|
|
52
|
+
final config = parseRecordConfig(args["configuration"]);
|
|
53
|
+
final upload = args["upload"];
|
|
54
|
+
final stream = control.hasEventHandler("stream");
|
|
55
|
+
if (config != null && await recorder!.hasPermission()) {
|
|
56
|
+
// If either upload or stream is requested, switch to the
|
|
57
|
+
// streaming code path (PCM chunks instead of file output).
|
|
58
|
+
if (upload != null || stream) {
|
|
59
|
+
return await _startStreamingRecording(config, upload, stream);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
final out = control.backend.getAssetSource(args["output_path"] ?? "");
|
|
63
|
+
if (!isWebPlatform() && !out.isFile) {
|
|
64
|
+
// on non-web platforms, the output path must be a valid file path
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await recorder!.start(config, path: out.path);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
case "stop_recording":
|
|
73
|
+
// For streaming recordings there is no output file to return; instead
|
|
74
|
+
// we stop the recorder and wait for the audio stream's `onDone` handler
|
|
75
|
+
// (`_finishStreamingRecording`) to flush and close the upload request.
|
|
76
|
+
if (_streamSession != null) {
|
|
77
|
+
await recorder!.stop();
|
|
78
|
+
await _streamSession?.completed.future;
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return await recorder!.stop();
|
|
82
|
+
case "cancel_recording":
|
|
83
|
+
// Tear down any in-flight streaming session before cancelling the
|
|
84
|
+
// recorder so partial uploads are aborted and listeners are notified.
|
|
85
|
+
if (_streamSession != null) {
|
|
86
|
+
await _cancelStreamingRecording("Recording cancelled");
|
|
87
|
+
}
|
|
88
|
+
await recorder!.cancel();
|
|
89
|
+
break;
|
|
90
|
+
case "resume_recording":
|
|
91
|
+
await recorder!.resume();
|
|
92
|
+
break;
|
|
93
|
+
case "pause_recording":
|
|
94
|
+
await recorder!.pause();
|
|
95
|
+
break;
|
|
96
|
+
case "is_supported_encoder":
|
|
97
|
+
var encoder = parseAudioEncoder(args["encoder"]);
|
|
98
|
+
if (encoder != null) {
|
|
99
|
+
return await recorder!.isEncoderSupported(encoder);
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case "is_paused":
|
|
103
|
+
return await recorder!.isPaused();
|
|
104
|
+
case "is_recording":
|
|
105
|
+
return await recorder!.isRecording();
|
|
106
|
+
case "has_permission":
|
|
107
|
+
return await recorder!.hasPermission();
|
|
108
|
+
case "get_input_devices":
|
|
109
|
+
List<InputDevice> devices = await recorder!.listInputDevices();
|
|
110
|
+
return devices.asMap().map((k, v) {
|
|
111
|
+
return MapEntry(v.id, v.label);
|
|
112
|
+
});
|
|
113
|
+
default:
|
|
114
|
+
throw Exception("Unknown AudioRecorder method: $name");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@override
|
|
119
|
+
void dispose() {
|
|
120
|
+
debugPrint("AudioRecorder(${control.id}).dispose()");
|
|
121
|
+
_onStateChangedSubscription?.cancel();
|
|
122
|
+
_recordStreamSubscription?.cancel();
|
|
123
|
+
_streamSession?.dispose();
|
|
124
|
+
recorder?.dispose();
|
|
125
|
+
control.removeInvokeMethodListener(_invokeMethod);
|
|
126
|
+
super.dispose();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Starts a streaming recording.
|
|
130
|
+
///
|
|
131
|
+
/// Depending on the arguments, the raw PCM chunks produced by the recorder
|
|
132
|
+
/// are either:
|
|
133
|
+
/// * forwarded to a remote HTTP endpoint via a chunked `StreamedRequest`
|
|
134
|
+
/// (when [uploadArgs] is provided), and/or
|
|
135
|
+
/// * pushed to Python as "stream" events (when [stream] is `true`).
|
|
136
|
+
///
|
|
137
|
+
/// Both sinks can be active at the same time. Returns `true` if the
|
|
138
|
+
/// recorder successfully started streaming, `false` otherwise.
|
|
139
|
+
Future<bool> _startStreamingRecording(
|
|
140
|
+
RecordConfig config,
|
|
141
|
+
Map<dynamic, dynamic>? uploadArgs,
|
|
142
|
+
bool stream,
|
|
143
|
+
) async {
|
|
144
|
+
// Defensive cleanup: ensure no previous streaming session is lingering
|
|
145
|
+
// before we start a new one (e.g. if the user restarts without stopping).
|
|
146
|
+
await _recordStreamSubscription?.cancel();
|
|
147
|
+
await _streamSession?.dispose();
|
|
148
|
+
_recordStreamSubscription = null;
|
|
149
|
+
_streamSession = null;
|
|
150
|
+
|
|
151
|
+
final uploadConfig = uploadArgs != null
|
|
152
|
+
? _UploadConfig.fromMap(Map<String, dynamic>.from(uploadArgs))
|
|
153
|
+
: null;
|
|
154
|
+
|
|
155
|
+
// Build a chunked HTTP request up front. We don't call `.send()` yet — that
|
|
156
|
+
// happens after the audio subscription is wired up so the first chunks are not lost.
|
|
157
|
+
http.StreamedRequest? request;
|
|
158
|
+
if (uploadConfig != null) {
|
|
159
|
+
final uploadUrl =
|
|
160
|
+
_getFullUploadUrl(control.backend.pageUri, uploadConfig.url);
|
|
161
|
+
request = http.StreamedRequest(uploadConfig.method, Uri.parse(uploadUrl));
|
|
162
|
+
if (uploadConfig.headers != null) {
|
|
163
|
+
request.headers.addAll(uploadConfig.headers!);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
final session = _StreamingSession(
|
|
168
|
+
stream: stream, uploadConfig: uploadConfig, request: request);
|
|
169
|
+
_streamSession = session;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
final audioStream = await recorder!.startStream(config);
|
|
173
|
+
|
|
174
|
+
// Emit an initial 0% progress event so listeners can show an upload
|
|
175
|
+
// started state before any bytes are produced by the microphone.
|
|
176
|
+
if (uploadConfig != null) {
|
|
177
|
+
_sendUploadEvent(
|
|
178
|
+
fileName: uploadConfig.fileName, progress: 0.0, bytesUploaded: 0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
_recordStreamSubscription = audioStream.listen(
|
|
182
|
+
(chunk) {
|
|
183
|
+
// For every audio chunk produced by the recorder: count it, feed
|
|
184
|
+
// it to the HTTP upload sink (if any), and forward it to Python
|
|
185
|
+
// (if a stream handler is subscribed).
|
|
186
|
+
session.bytesSent += chunk.length;
|
|
187
|
+
session.request?.sink.add(chunk);
|
|
188
|
+
|
|
189
|
+
if (session.request != null) {
|
|
190
|
+
_sendUploadEvent(
|
|
191
|
+
fileName: uploadConfig?.fileName,
|
|
192
|
+
bytesUploaded: session.bytesSent,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (session.stream) {
|
|
197
|
+
_sendStreamEvent(
|
|
198
|
+
chunk,
|
|
199
|
+
sequence: session.nextSequence(),
|
|
200
|
+
bytesStreamed: session.bytesSent,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
onError: (error) async {
|
|
205
|
+
if (uploadConfig != null) {
|
|
206
|
+
_sendUploadEvent(
|
|
207
|
+
fileName: uploadConfig.fileName,
|
|
208
|
+
error: error.toString(),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
await _cancelStreamingRecording();
|
|
212
|
+
},
|
|
213
|
+
onDone: () async {
|
|
214
|
+
// The recorder stopped normally — finalize the upload and notify.
|
|
215
|
+
await _finishStreamingRecording();
|
|
216
|
+
},
|
|
217
|
+
cancelOnError: true,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Kick off the HTTP request now that the sink will receive chunks.
|
|
221
|
+
session.startUpload();
|
|
222
|
+
return true;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
// Anything thrown while starting the stream (permissions, network,
|
|
225
|
+
// recorder errors) is reported and the session is discarded.
|
|
226
|
+
if (uploadConfig != null) {
|
|
227
|
+
_sendUploadEvent(
|
|
228
|
+
fileName: uploadConfig.fileName,
|
|
229
|
+
error: error.toString(),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
session.complete();
|
|
233
|
+
_streamSession = null;
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Finalizes a streaming recording after the audio stream's `onDone` fires.
|
|
239
|
+
///
|
|
240
|
+
/// Closes the HTTP request sink, awaits the server response, and emits a
|
|
241
|
+
/// final progress or error event to Python. Always resets the streaming
|
|
242
|
+
/// state, even on failure, so a new recording can be started afterwards.
|
|
243
|
+
Future<void> _finishStreamingRecording() async {
|
|
244
|
+
final session = _streamSession;
|
|
245
|
+
if (session == null) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
// Closing the sink signals the end of the chunked request body so the
|
|
251
|
+
// server can finish processing and return a response.
|
|
252
|
+
await session.request?.sink.close();
|
|
253
|
+
final responseFuture = session.responseFuture;
|
|
254
|
+
if (session.request != null && responseFuture != null) {
|
|
255
|
+
final response = await responseFuture;
|
|
256
|
+
// successful
|
|
257
|
+
if (response.statusCode >= 200 && response.statusCode <= 204) {
|
|
258
|
+
_sendUploadEvent(
|
|
259
|
+
fileName: session.uploadConfig?.fileName,
|
|
260
|
+
progress: 1.0,
|
|
261
|
+
bytesUploaded: session.bytesSent,
|
|
262
|
+
);
|
|
263
|
+
} else {
|
|
264
|
+
// not successful
|
|
265
|
+
final body = await http.Response.fromStream(response);
|
|
266
|
+
_sendUploadEvent(
|
|
267
|
+
fileName: session.uploadConfig?.fileName,
|
|
268
|
+
error:
|
|
269
|
+
"Upload endpoint returned code ${response.statusCode}: ${body.body}",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (session.uploadConfig != null) {
|
|
275
|
+
_sendUploadEvent(
|
|
276
|
+
fileName: session.uploadConfig?.fileName,
|
|
277
|
+
error: error.toString(),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
} finally {
|
|
281
|
+
// Whatever happened, release the subscription and signal the
|
|
282
|
+
// `stop_recording` awaiter that the session has fully wound down.
|
|
283
|
+
session.complete();
|
|
284
|
+
await _recordStreamSubscription?.cancel();
|
|
285
|
+
_recordStreamSubscription = null;
|
|
286
|
+
_streamSession = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/// Aborts the current streaming recording.
|
|
291
|
+
///
|
|
292
|
+
/// Called when the user cancels a recording or the audio stream emits an
|
|
293
|
+
/// error. Closes the upload sink without waiting for a response, notifies
|
|
294
|
+
/// Python with [error] (defaulting to "Recording cancelled"), and resets
|
|
295
|
+
/// the streaming state.
|
|
296
|
+
Future<void> _cancelStreamingRecording([String? error]) async {
|
|
297
|
+
final session = _streamSession;
|
|
298
|
+
if (session == null) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await _recordStreamSubscription?.cancel();
|
|
304
|
+
_recordStreamSubscription = null;
|
|
305
|
+
await session.request?.sink.close();
|
|
306
|
+
if (session.uploadConfig != null) {
|
|
307
|
+
_sendUploadEvent(
|
|
308
|
+
fileName: session.uploadConfig?.fileName,
|
|
309
|
+
error: error ?? "Recording cancelled",
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
} finally {
|
|
313
|
+
session.complete();
|
|
314
|
+
_streamSession = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/// Fires the "upload" event on the Python-side control with the current
|
|
319
|
+
/// upload progress or an error message. Any field may be null when not
|
|
320
|
+
/// applicable (e.g. `progress` is null for per-chunk progress pings).
|
|
321
|
+
void _sendUploadEvent({
|
|
322
|
+
String? fileName,
|
|
323
|
+
double? progress,
|
|
324
|
+
int? bytesUploaded,
|
|
325
|
+
String? error,
|
|
326
|
+
}) {
|
|
327
|
+
control.triggerEvent("upload", {
|
|
328
|
+
"file_name": fileName,
|
|
329
|
+
"progress": progress,
|
|
330
|
+
"bytes_uploaded": bytesUploaded,
|
|
331
|
+
"error": error,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// Fires the "stream" event with a single PCM [chunk] and its monotonically
|
|
336
|
+
/// increasing [sequence] number, allowing the Python side to reassemble the
|
|
337
|
+
/// audio in order and detect gaps.
|
|
338
|
+
void _sendStreamEvent(
|
|
339
|
+
Uint8List chunk, {
|
|
340
|
+
required int sequence,
|
|
341
|
+
required int bytesStreamed,
|
|
342
|
+
}) {
|
|
343
|
+
control.triggerEvent("stream", {
|
|
344
|
+
"chunk": chunk,
|
|
345
|
+
"sequence": sequence,
|
|
346
|
+
"bytes_streamed": bytesStreamed,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/// Resolves a possibly-relative [uploadUrl] against the current [pageUri].
|
|
351
|
+
///
|
|
352
|
+
/// If [uploadUrl] already contains an authority (scheme + host) it is used
|
|
353
|
+
/// verbatim; otherwise its path/query are combined with the page's
|
|
354
|
+
/// scheme/host/port so that relative upload endpoints work out of the box.
|
|
355
|
+
String _getFullUploadUrl(Uri pageUri, String uploadUrl) {
|
|
356
|
+
final uploadUri = Uri.parse(uploadUrl);
|
|
357
|
+
if (uploadUri.hasAuthority) {
|
|
358
|
+
return uploadUrl;
|
|
359
|
+
}
|
|
360
|
+
return Uri(
|
|
361
|
+
scheme: pageUri.scheme,
|
|
362
|
+
host: pageUri.host,
|
|
363
|
+
port: pageUri.port,
|
|
364
|
+
path: uploadUri.path,
|
|
365
|
+
query: uploadUri.query,
|
|
366
|
+
).toString();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/// Value object describing where/how the streamed recording should be
|
|
371
|
+
/// uploaded. Built from the `upload` dict passed from Python on `start_recording`.
|
|
372
|
+
class _UploadConfig {
|
|
373
|
+
const _UploadConfig({
|
|
374
|
+
required this.url,
|
|
375
|
+
required this.method,
|
|
376
|
+
this.headers,
|
|
377
|
+
this.fileName,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
/// Parses the raw map received from Python.
|
|
381
|
+
factory _UploadConfig.fromMap(Map<String, dynamic> value) {
|
|
382
|
+
final headers = value["headers"];
|
|
383
|
+
return _UploadConfig(
|
|
384
|
+
url: value["upload_url"],
|
|
385
|
+
method: (value["method"] ?? "PUT").toString().toUpperCase(),
|
|
386
|
+
headers: headers != null ? Map<String, String>.from(headers) : null,
|
|
387
|
+
fileName: value["file_name"],
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/// Destination URL — may be absolute or relative to the Flet page URI.
|
|
392
|
+
final String url;
|
|
393
|
+
|
|
394
|
+
/// HTTP method to use for the upload (e.g. `PUT` or `POST`).
|
|
395
|
+
final String method;
|
|
396
|
+
|
|
397
|
+
/// Optional request headers (e.g. auth, content-type).
|
|
398
|
+
final Map<String, String>? headers;
|
|
399
|
+
|
|
400
|
+
/// Optional file name echoed back in upload events so Python listeners
|
|
401
|
+
/// can correlate progress updates with a specific recording.
|
|
402
|
+
final String? fileName;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// Bundles everything needed to track a single streaming recording:
|
|
406
|
+
/// * whether Python is listening to `stream` events,
|
|
407
|
+
/// * the upload config and HTTP request (if uploading),
|
|
408
|
+
/// * byte/sequence counters, and
|
|
409
|
+
/// * a `Completer` used by `stop_recording` to await clean shutdown.
|
|
410
|
+
class _StreamingSession {
|
|
411
|
+
_StreamingSession({required this.stream, this.uploadConfig, this.request})
|
|
412
|
+
: completed = Completer<void>();
|
|
413
|
+
|
|
414
|
+
/// `true` when Python has a handler attached for the "stream" event.
|
|
415
|
+
final bool stream;
|
|
416
|
+
|
|
417
|
+
/// Upload target for this session, or `null` when only streaming to Python.
|
|
418
|
+
final _UploadConfig? uploadConfig;
|
|
419
|
+
|
|
420
|
+
/// Chunked HTTP request receiving the PCM bytes, or `null` when not uploading.
|
|
421
|
+
final http.StreamedRequest? request;
|
|
422
|
+
|
|
423
|
+
/// Resolves once the session has fully torn down (success, error, or
|
|
424
|
+
/// cancellation). Awaited by `stop_recording` so callers can rely on the
|
|
425
|
+
/// upload being flushed before the method returns.
|
|
426
|
+
final Completer<void> completed;
|
|
427
|
+
|
|
428
|
+
/// Future of the server response to the chunked upload. Populated by
|
|
429
|
+
/// [startUpload] and awaited by `_finishStreamingRecording`.
|
|
430
|
+
Future<http.StreamedResponse>? responseFuture;
|
|
431
|
+
|
|
432
|
+
/// Total number of audio bytes produced so far — used for progress events
|
|
433
|
+
/// and as the final uploaded-bytes count.
|
|
434
|
+
int bytesSent = 0;
|
|
435
|
+
|
|
436
|
+
/// Monotonic counter for stream events, incremented by [nextSequence].
|
|
437
|
+
int _sequence = 0;
|
|
438
|
+
|
|
439
|
+
/// Starts sending the chunked request. Must be called after the audio
|
|
440
|
+
/// subscription has been attached so no chunks are dropped.
|
|
441
|
+
void startUpload() {
|
|
442
|
+
if (request != null) {
|
|
443
|
+
responseFuture = request!.send();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/// Returns the next sequence number for a "stream" event (starts at 1).
|
|
448
|
+
int nextSequence() {
|
|
449
|
+
_sequence += 1;
|
|
450
|
+
return _sequence;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/// Marks the session as fully wound down. Safe to call multiple times.
|
|
454
|
+
void complete() {
|
|
455
|
+
if (!completed.isCompleted) {
|
|
456
|
+
completed.complete();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/// Best-effort cleanup used when the service itself is being disposed
|
|
461
|
+
/// (e.g. the control is removed from the page mid-recording).
|
|
462
|
+
Future<void> dispose() async {
|
|
463
|
+
try {
|
|
464
|
+
await request?.sink.close();
|
|
465
|
+
} catch (_) {
|
|
466
|
+
// Ignore sink shutdown errors during service disposal.
|
|
467
|
+
}
|
|
468
|
+
complete();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
flet==0.85.0.dev2
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import 'dart:async';
|
|
2
|
-
|
|
3
|
-
import 'package:flet/flet.dart';
|
|
4
|
-
import 'package:flutter/widgets.dart';
|
|
5
|
-
import 'package:record/record.dart';
|
|
6
|
-
|
|
7
|
-
import 'utils/audio_recorder.dart';
|
|
8
|
-
|
|
9
|
-
class AudioRecorderService extends FletService {
|
|
10
|
-
AudioRecorderService({required super.control});
|
|
11
|
-
|
|
12
|
-
AudioRecorder? recorder;
|
|
13
|
-
StreamSubscription? _onStateChangedSubscription;
|
|
14
|
-
|
|
15
|
-
@override
|
|
16
|
-
void init() {
|
|
17
|
-
super.init();
|
|
18
|
-
debugPrint("AudioRecorder.init($hashCode)");
|
|
19
|
-
control.addInvokeMethodListener(_invokeMethod);
|
|
20
|
-
|
|
21
|
-
recorder = AudioRecorder();
|
|
22
|
-
|
|
23
|
-
_onStateChangedSubscription = recorder!.onStateChanged().listen((state) {
|
|
24
|
-
_onStateChanged.call(state);
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
void _onStateChanged(RecordState state) {
|
|
29
|
-
var stateMap = {
|
|
30
|
-
RecordState.record: "recording",
|
|
31
|
-
RecordState.pause: "paused",
|
|
32
|
-
RecordState.stop: "stopped",
|
|
33
|
-
};
|
|
34
|
-
control.triggerEvent("state_change", stateMap[state]);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
Future<dynamic> _invokeMethod(String name, dynamic args) async {
|
|
38
|
-
debugPrint("AudioRecorder.$name($args)");
|
|
39
|
-
switch (name) {
|
|
40
|
-
case "start_recording":
|
|
41
|
-
final config = parseRecordConfig(args["configuration"]);
|
|
42
|
-
if (config != null && await recorder!.hasPermission()) {
|
|
43
|
-
final out = control.backend.getAssetSource(args["output_path"] ?? "");
|
|
44
|
-
if (!isWebPlatform() && !out.isFile) {
|
|
45
|
-
// on non-web/IO platforms, the output path must be a valid file path
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
await recorder!.start(config, path: out.path);
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
return false;
|
|
53
|
-
case "stop_recording":
|
|
54
|
-
return await recorder!.stop();
|
|
55
|
-
case "cancel_recording":
|
|
56
|
-
await recorder!.cancel();
|
|
57
|
-
case "resume_recording":
|
|
58
|
-
await recorder!.resume();
|
|
59
|
-
case "pause_recording":
|
|
60
|
-
await recorder!.pause();
|
|
61
|
-
case "is_supported_encoder":
|
|
62
|
-
var encoder = parseAudioEncoder(args["encoder"]);
|
|
63
|
-
if (encoder != null) {
|
|
64
|
-
return await recorder!.isEncoderSupported(encoder);
|
|
65
|
-
}
|
|
66
|
-
break;
|
|
67
|
-
case "is_paused":
|
|
68
|
-
return await recorder!.isPaused();
|
|
69
|
-
case "is_recording":
|
|
70
|
-
return await recorder!.isRecording();
|
|
71
|
-
case "has_permission":
|
|
72
|
-
return await recorder!.hasPermission();
|
|
73
|
-
case "get_input_devices":
|
|
74
|
-
List<InputDevice> devices = await recorder!.listInputDevices();
|
|
75
|
-
return devices.asMap().map((k, v) {
|
|
76
|
-
return MapEntry(v.id, v.label);
|
|
77
|
-
});
|
|
78
|
-
default:
|
|
79
|
-
throw Exception("Unknown AudioRecorder method: $name");
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@override
|
|
84
|
-
void dispose() {
|
|
85
|
-
debugPrint("AudioRecorder(${control.id}).dispose()");
|
|
86
|
-
_onStateChangedSubscription?.cancel();
|
|
87
|
-
recorder?.dispose();
|
|
88
|
-
control.removeInvokeMethodListener(_invokeMethod);
|
|
89
|
-
super.dispose();
|
|
90
|
-
}
|
|
91
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|