Simulcast, Screen sharing & Various improvements (#4)
* Respect `RTCIceTransportPolicy` enum and organize * Simplify syntax where possible etc. * Combine `VideoPreset` and `VideoPresets` * Default values for `ConnectOptions` * Build URI instead of String manipulation * Slight modifications to Exception * `LiveKitTheme` for example * `VideoEncoding` class * Organize imports * First simulcast implementation * Remove unnecessary try-catches * Update Android settings * Remember uri and token * example improvements * `fit` parameter for VideoTrackRenderer * Simulcast option for example * Pass `defaultPublishOptions` * Show only `VideoQuality` * Pass tests * Better buildUri logic * Named parameter to positional * Explicit imports * `VideoParameter` instead of `VideoPreset` * Use `mediaTrack.getSettings` when possible * Safer dispose logic * Safer `PCTransport` Update transport.dart * Synchronized events for `SignalClient` * Use logger instead of print * Make example compile for iOS * First screen share implementation * Make example work with screen share * Example improvement * Code optimization * Don't depend on web_socket_channel * Fix: Unpublish track bug * Show participant mute state & identity * Update protos * Remote mute/unmute * iOS Background mode * Separate `createCameraTrack` and `createScreenTrack` * Clean up * PB fix * format * Fix analyzer warning * Android clean up * Update README.md * Clean up
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../exts.dart';
|
||||
import 'room.dart';
|
||||
|
||||
class ConnectPage extends StatefulWidget {
|
||||
//
|
||||
const ConnectPage({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ConnectPageState();
|
||||
}
|
||||
|
||||
class _ConnectPageState extends State<ConnectPage> {
|
||||
//
|
||||
static const _storeKeyUri = 'uri';
|
||||
static const _storeKeyToken = 'token';
|
||||
static const _storeKeySimulcast = 'simulcast';
|
||||
|
||||
final _uriCtrl = TextEditingController();
|
||||
final _tokenCtrl = TextEditingController();
|
||||
bool _simulcast = false;
|
||||
bool _busy = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_readPrefs();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_uriCtrl.dispose();
|
||||
_tokenCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _readPrefs() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_uriCtrl.text = prefs.getString(_storeKeyUri) ?? '';
|
||||
_tokenCtrl.text = prefs.getString(_storeKeyToken) ?? '';
|
||||
setState(() {
|
||||
_simulcast = prefs.getBool(_storeKeySimulcast) ?? false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _writePrefs() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_storeKeyUri, _uriCtrl.text);
|
||||
await prefs.setString(_storeKeyToken, _tokenCtrl.text);
|
||||
await prefs.setBool(_storeKeySimulcast, _simulcast);
|
||||
}
|
||||
|
||||
Future<void> _connect(BuildContext ctx) async {
|
||||
//
|
||||
try {
|
||||
setState(() {
|
||||
_busy = true;
|
||||
});
|
||||
|
||||
print('Connecting with url: ${_uriCtrl.text}, token: ${_tokenCtrl.text}...');
|
||||
|
||||
final room = await LiveKitClient.connect(
|
||||
_uriCtrl.text,
|
||||
_tokenCtrl.text,
|
||||
options: ConnectOptions(
|
||||
defaultPublishOptions: TrackPublishOptions(
|
||||
simulcast: _simulcast,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Save for next time
|
||||
await _writePrefs();
|
||||
|
||||
await Navigator.push<void>(
|
||||
ctx,
|
||||
MaterialPageRoute(builder: (_) => RoomPage(room)),
|
||||
);
|
||||
} catch (error) {
|
||||
print('could not connect $error');
|
||||
await ctx.showErrorDialog(error);
|
||||
} finally {
|
||||
setState(() {
|
||||
_busy = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _setSimulcast(bool? value) async {
|
||||
if (value == null || _simulcast == value) return;
|
||||
setState(() {
|
||||
_simulcast = value;
|
||||
});
|
||||
// await _writePrefs();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Connect to LiveKit'),
|
||||
),
|
||||
body: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 20,
|
||||
horizontal: 20,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.secondary),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 320,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _uriCtrl,
|
||||
decoration: const InputDecoration(labelText: 'URL'),
|
||||
),
|
||||
TextField(
|
||||
controller: _tokenCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Token'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: CheckboxListTile(
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
onChanged: (value) => _setSimulcast(value),
|
||||
title: const Text('Use Simulcast'),
|
||||
value: _simulcast,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: ElevatedButton(
|
||||
onPressed: _busy ? null : () => _connect(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_busy)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 10),
|
||||
child: SizedBox(
|
||||
height: 15,
|
||||
width: 15,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text('Connect'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../widgets/controls.dart';
|
||||
import '../widgets/participant.dart';
|
||||
|
||||
class RoomPage extends StatefulWidget {
|
||||
//
|
||||
final Room room;
|
||||
|
||||
const RoomPage(
|
||||
this.room, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _RoomPageState();
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomPageState extends State<RoomPage> with RoomDelegate {
|
||||
// BuildContext? _lastContext;
|
||||
//
|
||||
List<Participant> participants = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.room.delegate = this;
|
||||
widget.room.addListener(_onChange);
|
||||
_onConnected();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.room.delegate = null;
|
||||
widget.room.removeListener(_onChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onConnected() async {
|
||||
// video will fail when running in ios simulator
|
||||
try {
|
||||
final localVideo = await LocalVideoTrack.createCameraTrack(); // Defaults to camera
|
||||
await widget.room.localParticipant.publishVideoTrack(
|
||||
localVideo,
|
||||
// options: TrackPublishOptions(
|
||||
// // simulcast: true,
|
||||
// videoEncoding: VideoParameters.presetQVGA169.encoding,
|
||||
// ),
|
||||
);
|
||||
} catch (e) {
|
||||
print('could not publish video: $e');
|
||||
}
|
||||
|
||||
final localAudio = await LocalAudioTrack.create();
|
||||
await widget.room.localParticipant.publishAudioTrack(localAudio);
|
||||
sortParticipants();
|
||||
}
|
||||
|
||||
void _onChange() {
|
||||
sortParticipants();
|
||||
}
|
||||
|
||||
void sortParticipants() {
|
||||
List<Participant> participants = [];
|
||||
participants.addAll(widget.room.participants.values);
|
||||
// sort speakers for the grid
|
||||
participants.sort((a, b) {
|
||||
// loudest speaker first
|
||||
if (a.isSpeaking && b.isSpeaking) {
|
||||
if (a.audioLevel > b.audioLevel) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// last spoken at
|
||||
final aSpokeAt = a.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
||||
final bSpokeAt = b.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
||||
|
||||
if (aSpokeAt != bSpokeAt) {
|
||||
return aSpokeAt > bSpokeAt ? -1 : 1;
|
||||
}
|
||||
|
||||
// video on
|
||||
if (a.hasVideo != b.hasVideo) {
|
||||
return a.hasVideo ? -1 : 1;
|
||||
}
|
||||
|
||||
// joinedAt
|
||||
return a.joinedAt.millisecondsSinceEpoch - b.joinedAt.millisecondsSinceEpoch;
|
||||
});
|
||||
|
||||
if (participants.length > 1) {
|
||||
participants.insert(1, widget.room.localParticipant);
|
||||
} else {
|
||||
participants.add(widget.room.localParticipant);
|
||||
}
|
||||
setState(() {
|
||||
this.participants = participants;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onDisconnected() {
|
||||
// final context = _lastContext;
|
||||
print('disconnected: $context');
|
||||
// if (context != null) {
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: ControlsWidget(widget.room),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user