Switch to new event system (#9)
* `SignalEvents` and `synchronized` mode for `EventsListenable` * cascade syntax * clean up * `RoomEvents` * `ParticipantEvents` * all events implemented * make it compile with M1 macs * dispose logic * cleaner connect logic * ask to publish * fix initial EngineTrackAddedEvent glitch * fix unpublishTrack bug * clean up * better events docs * organize event types * clean up * cleaner wait logic * wait for event instead of using Completer * notifyListeners
This commit is contained in:
+19
-1
@@ -2,6 +2,24 @@ import 'package:flutter/material.dart';
|
||||
|
||||
extension LKExampleExt on BuildContext {
|
||||
//
|
||||
Future<bool?> showPublishDialog() => showDialog<bool>(
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Publish'),
|
||||
content: const Text('Would you like to publish your Camera & Mic ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('NO'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('YES'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> showErrorDialog(dynamic exception) => showDialog<void>(
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
@@ -74,7 +92,7 @@ extension LKExampleExt on BuildContext {
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Received data'),
|
||||
content: Text('"${data}"'),
|
||||
content: Text(data),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:livekit_example/theme.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'pages/connect.dart';
|
||||
|
||||
void main() {
|
||||
print('This is a test for ${SignalTrickleEvent} test.');
|
||||
// configure logs for debugging
|
||||
Logger.root.level = Level.FINE;
|
||||
Logger.root.onRecord.listen((record) {
|
||||
|
||||
@@ -62,6 +62,9 @@ class _ConnectPageState extends State<ConnectPage> {
|
||||
_busy = true;
|
||||
});
|
||||
|
||||
// Save for next time
|
||||
await _writePrefs();
|
||||
|
||||
print('Connecting with url: ${_uriCtrl.text}, token: ${_tokenCtrl.text}...');
|
||||
|
||||
final room = await LiveKitClient.connect(
|
||||
@@ -74,9 +77,6 @@ class _ConnectPageState extends State<ConnectPage> {
|
||||
),
|
||||
);
|
||||
|
||||
// Save for next time
|
||||
await _writePrefs();
|
||||
|
||||
await Navigator.push<void>(
|
||||
ctx,
|
||||
MaterialPageRoute(builder: (_) => RoomPage(room)),
|
||||
@@ -122,10 +122,14 @@ class _ConnectPageState extends State<ConnectPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
controller: _uriCtrl,
|
||||
decoration: const InputDecoration(labelText: 'URL'),
|
||||
),
|
||||
TextField(
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
controller: _tokenCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Token'),
|
||||
),
|
||||
|
||||
+64
-57
@@ -2,12 +2,12 @@ import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../exts.dart';
|
||||
import '../widgets/controls.dart';
|
||||
import '../widgets/participant.dart';
|
||||
import '../exts.dart';
|
||||
|
||||
class RoomPage extends StatefulWidget {
|
||||
//
|
||||
@@ -22,29 +22,51 @@ class RoomPage extends StatefulWidget {
|
||||
State<StatefulWidget> createState() => _RoomPageState();
|
||||
}
|
||||
|
||||
class _RoomPageState extends State<RoomPage> with RoomDelegate {
|
||||
class _RoomPageState extends State<RoomPage> {
|
||||
//
|
||||
List<Participant> participants = [];
|
||||
late final _listener = EventsListener<LiveKitEvent>(widget.room.events);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.room.delegate = this;
|
||||
widget.room.addListener(_onChange);
|
||||
_onConnected();
|
||||
widget.room.addListener(_onRoomDidUpdate);
|
||||
_setUpListeners();
|
||||
_sortParticipants();
|
||||
WidgetsBinding.instance?.addPostFrameCallback((_) => _askPublish());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.room.delegate = null;
|
||||
widget.room.removeListener(_onChange);
|
||||
// always dispose listener
|
||||
(() async {
|
||||
widget.room.removeListener(_onRoomDidUpdate);
|
||||
await _listener.dispose();
|
||||
await widget.room.dispose();
|
||||
})();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onConnected() async {
|
||||
void _setUpListeners() => _listener
|
||||
..on<RoomDisconnectedEvent>((_) => Navigator.pop(context))
|
||||
..on<DataReceivedEvent>((event) {
|
||||
String decoded = 'Failed to decode';
|
||||
try {
|
||||
decoded = utf8.decode(event.data);
|
||||
} catch (_) {
|
||||
print('Failed to decode: $_');
|
||||
}
|
||||
context.showDataReceivedDialog(decoded);
|
||||
});
|
||||
|
||||
void _askPublish() async {
|
||||
final result = await context.showPublishDialog();
|
||||
if (result != true) return;
|
||||
// video will fail when running in ios simulator
|
||||
try {
|
||||
final localVideo = await LocalVideoTrack.createCameraTrack(); // Defaults to camera
|
||||
// Create video track
|
||||
final localVideo = await LocalVideoTrack.createCameraTrack();
|
||||
// Try to publish the video
|
||||
await widget.room.localParticipant.publishVideoTrack(
|
||||
localVideo,
|
||||
// options: TrackPublishOptions(
|
||||
@@ -52,20 +74,22 @@ class _RoomPageState extends State<RoomPage> with RoomDelegate {
|
||||
// videoEncoding: VideoParameters.presetQVGA169.encoding,
|
||||
// ),
|
||||
);
|
||||
} catch (e) {
|
||||
print('could not publish video: $e');
|
||||
|
||||
// Create mic track
|
||||
final localAudio = await LocalAudioTrack.create();
|
||||
// // Try to publish audio
|
||||
await widget.room.localParticipant.publishAudioTrack(localAudio);
|
||||
} catch (error) {
|
||||
print('could not publish video: $error');
|
||||
await context.showErrorDialog(error);
|
||||
}
|
||||
|
||||
final localAudio = await LocalAudioTrack.create();
|
||||
await widget.room.localParticipant.publishAudioTrack(localAudio);
|
||||
sortParticipants();
|
||||
}
|
||||
|
||||
void _onChange() {
|
||||
sortParticipants();
|
||||
void _onRoomDidUpdate() {
|
||||
_sortParticipants();
|
||||
}
|
||||
|
||||
void sortParticipants() {
|
||||
void _sortParticipants() {
|
||||
List<Participant> participants = [];
|
||||
participants.addAll(widget.room.participants.values);
|
||||
// sort speakers for the grid
|
||||
@@ -106,48 +130,31 @@ class _RoomPageState extends State<RoomPage> with RoomDelegate {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onDataReceived(RemoteParticipant participant, List<int> data) async {
|
||||
await context.showDataReceivedDialog(utf8.decode(data));
|
||||
}
|
||||
|
||||
@override
|
||||
void onDisconnected() {
|
||||
print('disconnected: $context');
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
// with a provider, any child/descendent widget can be updated if they
|
||||
// are a Consumer of Room.
|
||||
body: ChangeNotifierProvider.value(
|
||||
value: widget.room,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: participants.isNotEmpty
|
||||
? ParticipantWidget(participants.first)
|
||||
: Container()),
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: math.max(0, participants.length - 1),
|
||||
itemBuilder: (BuildContext context, int index) => Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: ParticipantWidget(participants[index + 1], quality: VideoQuality.LOW),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child:
|
||||
participants.isNotEmpty ? ParticipantWidget(participants.first) : Container()),
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: math.max(0, participants.length - 1),
|
||||
itemBuilder: (BuildContext context, int index) => Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: ParticipantWidget(participants[index + 1], quality: VideoQuality.LOW),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: ControlsWidget(widget.room),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: ControlsWidget(widget.room),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,8 +102,8 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
//
|
||||
final lp = widget.room.localParticipant;
|
||||
|
||||
for (final tracks in lp.videoTracks) {
|
||||
await lp.unpublishTrack(tracks.track!);
|
||||
for (final track in lp.videoTracks) {
|
||||
await lp.unpublishTrack(track.sid);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -21,7 +21,7 @@ class ParticipantWidget extends StatefulWidget {
|
||||
State<StatefulWidget> createState() => _ParticipantWidgetState();
|
||||
}
|
||||
|
||||
class _ParticipantWidgetState extends State<ParticipantWidget> with ParticipantDelegate {
|
||||
class _ParticipantWidgetState extends State<ParticipantWidget> {
|
||||
//
|
||||
TrackPublication? videoPub;
|
||||
TrackPublication? audioPub;
|
||||
|
||||
Reference in New Issue
Block a user