Skip to content

Commit

Permalink
Break apart state and add mute support (#6)
Browse files Browse the repository at this point in the history
Breaking apart the state object into several notifiers allows for more
efficient UI re-renders (and pushes in the direction I think all our
SDKs should follow).
  • Loading branch information
mdepinet committed Sep 17, 2024
1 parent f022bf4 commit 7f0e0d7
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 140 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@
## 0.0.2

* Add support for experimental messages.

## 0.0.3

* Add mute/unmute support.
* Break apart state into separate notifiers.
* Update example app to take advantage of both (plus sendText).
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ dependencies:
```dart
final session = UltravoxSession.create();
final state = await session.joinCall(joinUrl);
state.addListener(myListener);
await session.joinCall(joinUrl);
session.statusNotifier.addListener(myListener);
await session.leaveCall();
```

See the included example app for a more complete example. To get a `joinUrl`, you'll want to integrate your server with the [Ultravox REST API](https://fixie-ai.github.io/ultradox/).
Expand Down
140 changes: 110 additions & 30 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,25 @@ class MyHomePage extends StatefulWidget {

class _MyHomePageState extends State<MyHomePage> {
UltravoxSession? _session;
bool debug = false;
bool _debug = false;
bool _connected = false;

@override
void dispose() {
if (_session != null) {
_session!.state.removeListener(_onStateChange);
_session!.statusNotifier.removeListener(_onStatusChange);
unawaited(_session!.leaveCall());
}
super.dispose();
}

void _onStateChange() {
// Refresh the UI when the session state changes.
setState(() {});
void _onStatusChange() {
if (_session?.status.live != _connected) {
// Refresh the UI when we connect and disconnect.
setState(() {
_connected = _session?.status.live ?? false;
});
}
}

Future<void> _startCall(String joinUrl) async {
Expand All @@ -62,17 +67,17 @@ class _MyHomePageState extends State<MyHomePage> {
}
setState(() {
_session =
UltravoxSession.create(experimentalMessages: debug ? {"debug"} : {});
UltravoxSession.create(experimentalMessages: _debug ? {"debug"} : {});
});
_session!.state.addListener(_onStateChange);
_session!.statusNotifier.addListener(_onStatusChange);
await _session!.joinCall(joinUrl);
}

Future<void> _endCall() async {
if (_session == null) {
return;
}
_session!.state.removeListener(_onStateChange);
_session!.statusNotifier.removeListener(_onStatusChange);
await _session!.leaveCall();
setState(() {
_session = null;
Expand Down Expand Up @@ -104,20 +109,21 @@ class _MyHomePageState extends State<MyHomePage> {
text: 'Debug',
style: TextStyle(fontWeight: FontWeight.bold))),
Switch(
value: debug,
onChanged: (value) => setState(() => debug = value),
value: _debug,
onChanged: (value) => setState(() => _debug = value),
),
const Spacer(),
ElevatedButton(
ElevatedButton.icon(
icon: const Icon(Icons.call),
onPressed: () => _startCall(textController.text),
child: const Text('Start Call'),
label: const Text('Start Call'),
),
],
)
],
),
));
} else if (!_session!.state.status.live) {
} else if (!_connected) {
mainBodyChildren.add(const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
Expand All @@ -129,31 +135,105 @@ class _MyHomePageState extends State<MyHomePage> {
} else {
mainBodyChildren.add(
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: ListView(
reverse: true, // Fill from bottom, clip at top.
children: [
for (final transcript in _session!.state.transcripts.reversed)
TranscriptWidget(transcript: transcript),
]),
),
constraints: const BoxConstraints(maxHeight: 200),
child: ListenableBuilder(
listenable: _session!.transcriptsNotifier,
builder: (BuildContext context, Widget? child) {
return ListView(
reverse: true, // Fill from bottom, clip at top.
children: [
for (final transcript in _session!.transcripts.reversed)
TranscriptWidget(transcript: transcript),
]);
})),
);
mainBodyChildren.add(
ElevatedButton(
onPressed: _endCall,
child: const Text('End Call'),
final textController = TextEditingController();
final textInput = TextField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
controller: textController,
);
if (debug) {
mainBodyChildren.add(Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(child: textInput),
ElevatedButton.icon(
icon: const Icon(Icons.send),
onPressed: () {
_session!.sendText(textController.text);
textController.clear();
},
label: const Text('Send'),
),
],
));
mainBodyChildren.add(const SizedBox(height: 20));
mainBodyChildren.add(Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
ListenableBuilder(
listenable: _session!.userMutedNotifier,
builder: (BuildContext context, Widget? child) {
return ElevatedButton.icon(
icon: _session!.userMuted
? const Icon(Icons.mic_off)
: const Icon(Icons.mic),
onPressed: () {
if (_session!.userMuted) {
_session!.unmute({Role.user});
} else {
_session!.mute({Role.user});
}
},
label: _session!.userMuted
? const Text('Unmute')
: const Text('Mute'),
);
}),
ListenableBuilder(
listenable: _session!.agentMutedNotifier,
builder: (BuildContext context, Widget? child) {
return ElevatedButton.icon(
icon: _session!.agentMuted
? const Icon(Icons.volume_off)
: const Icon(Icons.volume_up),
onPressed: () {
if (_session!.agentMuted) {
_session!.unmute({Role.agent});
} else {
_session!.mute({Role.agent});
}
},
label: _session!.agentMuted
? const Text('Unmute Agent')
: const Text('Mute Agent'),
);
}),
ElevatedButton.icon(
icon: const Icon(Icons.call_end),
onPressed: _endCall,
label: const Text('End Call'),
),
],
));
if (_debug) {
mainBodyChildren.add(const SizedBox(height: 20));
mainBodyChildren.add(const Text.rich(TextSpan(
text: 'Last Debug Message:',
style: TextStyle(fontWeight: FontWeight.w700))));

if (_session!.state.lastExperimentalMessage != null) {
mainBodyChildren.add(DebugMessageWidget(
message: _session!.state.lastExperimentalMessage!));
}
mainBodyChildren.add(ListenableBuilder(
listenable: _session!.experimentalMessageNotifier,
builder: (BuildContext context, Widget? child) {
final message = _session!.lastExperimentalMessage;
if (message.containsKey("type") && message["type"] == "debug") {
return DebugMessageWidget(message: message);
} else {
return const SizedBox(height: 20);
}
},
));
}
}
return Scaffold(
Expand Down
26 changes: 13 additions & 13 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ packages:
dependency: transitive
description:
name: dart_webrtc
sha256: ac7ef077084b3e54004716f1d736fcd839e1b60bc3f21f4122a35a9bb5ca2e47
sha256: c664ad88d5646735753add421ee2118486c100febef5e92b7f59cdbabf6a51f6
url: "https://pub.dev"
source: hosted
version: "1.4.8"
version: "1.4.9"
dbus:
dependency: transitive
description:
Expand Down Expand Up @@ -172,10 +172,10 @@ packages:
dependency: transitive
description:
name: flutter_webrtc
sha256: "67faa07cf49392b50b1aa14590a83caa64d2109345fabd29899dcd8da8538348"
sha256: f6800cc2af79018c12e955ddf8ad007891fdfbb8199b0ce3dccd0977ed2add9c
url: "https://pub.dev"
source: hosted
version: "0.11.6+hotfix.1"
version: "0.11.7"
http:
dependency: transitive
description:
Expand Down Expand Up @@ -236,10 +236,10 @@ packages:
dependency: transitive
description:
name: livekit_client
sha256: fc86a8b65b74b41faef646cc671c1892a457c24fd69910e25f7a50dc8cdd3155
sha256: "5df9b6f153b5f2c59fbf116b41e54597dfe8b2340b6630f7d8869887a9e58f44"
url: "https://pub.dev"
source: hosted
version: "2.2.4"
version: "2.2.5"
logging:
dependency: transitive
description:
Expand Down Expand Up @@ -441,10 +441,10 @@ packages:
dependency: transitive
description:
name: synchronized
sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255
sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.3.0+2"
term_glyph:
dependency: transitive
description:
Expand Down Expand Up @@ -475,7 +475,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.0.1"
version: "0.0.3"
uuid:
dependency: transitive
description:
Expand Down Expand Up @@ -504,10 +504,10 @@ packages:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "1.0.0"
web_socket:
dependency: transitive
description:
Expand Down Expand Up @@ -544,10 +544,10 @@ packages:
dependency: transitive
description:
name: win32_registry
sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6"
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
url: "https://pub.dev"
source: hosted
version: "1.1.4"
version: "1.1.5"
xdg_directories:
dependency: transitive
description:
Expand Down
Loading

0 comments on commit 7f0e0d7

Please sign in to comment.