-
Notifications
You must be signed in to change notification settings - Fork 1
/
voice_reminder.dart
147 lines (132 loc) · 4.9 KB
/
voice_reminder.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import 'package:asterisk/asterisk.dart';
import 'package:async/async.dart';
import '_credentials.dart';
/// A simple voice-based timer system.
///
/// This accepts incoming calls, in which the caller is expected to enter a
/// duration in seconds using DTMF codes, followed by a number sign (`#`).
/// After doing that, the rest of the incoming call is recorded. After it ends,
/// this waits for the indicate duration and then calls back to play the
/// recording as a reminder.
void main() async {
final asterisk = createAsteriskClient();
print('Starting up - dial number 1 to set reminders');
print('After the call is accepted, enter the time in seconds and press #.');
print('You can then record a reminder - the timer starts as soon as you');
print('hang up the call');
await for (final incoming in asterisk.stasisStart) {
if (incoming.args.isEmpty) {
// Ignore requests with arguments - these are outgoing channels placed
// into the application and not an incoming channel to handle
VoiceReminder(asterisk).run(incoming.channel);
}
}
}
class VoiceReminder {
final Asterisk asterisk;
final StreamGroup<VoiceCallEvent> _events = StreamGroup();
int durationInSeconds = 0;
String? recordingName;
LiveChannel? outgoing;
bool outgoingPickedUp = false;
VoiceReminder(this.asterisk);
Future<void> run(LiveChannel incoming) async {
await incoming.answer();
final incomingEvents = incoming.events
.map((event) {
if (event case ChannelDtmfReceived(digit: final digit)) {
return EnteredDigit(char: digit);
}
if (event is StasisEnd) {
return HungUp();
}
})
.where((e) => e is VoiceCallEvent)
.cast<VoiceCallEvent>();
_events.add(incomingEvents);
await for (final event in _events.stream) {
switch (event) {
case EnteredDigit(char: '#'):
// Start live recording for playback.
final name = recordingName = incoming.channel.id;
await incoming.record(
name: name,
format: 'wav',
ifExists: RecordingExistsBehavior.overwrite,
);
break;
case EnteredDigit(char: final digit):
// Append digit to timer
try {
durationInSeconds = durationInSeconds * 10 + int.parse(digit);
} on FormatException {
// ignore
}
case HungUp():
if (outgoing != null) {
// The outgoing channel to which we're playing the recording hung
// up. Delete the recording so that we don't leak disk space.
await asterisk.api.recordings.deleteStored(recordingName!);
} else if (durationInSeconds > 0) {
// The incoming call hung up after we've started the voice
// recording. Start the timer now!
_events.add(
Stream.fromFuture(Future.delayed(
Duration(seconds: durationInSeconds),
() => TimerExpired(),
)),
);
}
_events.remove(incomingEvents);
case ReplayFinished():
await outgoing?.hangUp();
return;
case TimerExpired():
// Call the user again, and play back the recorded timer.
final channel = outgoing = await asterisk.createChannel(
endpoint: 'PJSIP/${incoming.channel.caller.number}',
appArgs: 'outgoing',
formats: ['opus', 'ulaw'],
variables: {
'CALLERID(name)': 'Scheduled reminder',
'CALLERID(number)': '1',
},
);
await channel.dial(timeout: const Duration(minutes: 1));
_events.add(channel.events
.map((event) {
if (event is StasisEnd) {
return HungUp();
} else if (event is ChannelStateChange &&
!outgoingPickedUp &&
channel.channel.state == ChannelState.up) {
outgoingPickedUp = true;
return PickedUp();
}
})
.where((e) => e is VoiceCallEvent)
.cast());
case PickedUp():
final playback = await outgoing!
.play(sources: [MediaSource.recording(recordingName!)]);
_events.add(playback.events
.map((event) {
if (event is PlaybackFinished) {
return ReplayFinished();
}
})
.where((e) => e is VoiceCallEvent)
.cast());
}
}
}
}
sealed class VoiceCallEvent {}
final class EnteredDigit implements VoiceCallEvent {
final String char;
EnteredDigit({required this.char});
}
final class HungUp implements VoiceCallEvent {}
final class PickedUp implements VoiceCallEvent {}
final class ReplayFinished implements VoiceCallEvent {}
final class TimerExpired implements VoiceCallEvent {}