example app design update & show active speaker

This commit is contained in:
Hiroshi Horie
2021-10-25 02:08:26 +09:00
parent 6d4f32233f
commit d065c2eb97
11 changed files with 314 additions and 160 deletions
+98 -60
View File
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:livekit_example/widgets/text_field.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../exts.dart';
@@ -105,74 +107,110 @@ class _ConnectPageState extends State<ConnectPage> {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Connect to LiveKit'),
),
body: Center(
body: Container(
alignment: Alignment.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,
),
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
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'),
),
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'),
],
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 70),
child: SvgPicture.asset(
'images/logo-dark.svg',
),
),
Padding(
padding: const EdgeInsets.only(bottom: 30),
child: LKTextField(
label: 'Server URL',
ctrl: _uriCtrl,
),
),
Padding(
padding: const EdgeInsets.only(bottom: 50),
child: LKTextField(
label: 'Token',
ctrl: _tokenCtrl,
),
),
Container(
alignment: Alignment.center,
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'),
],
),
),
),
// 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(
// enableSuggestions: false,
// autocorrect: false,
// controller: _uriCtrl,
// decoration: const InputDecoration(labelText: 'URL'),
// ),
// TextField(
// enableSuggestions: false,
// autocorrect: false,
// 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,
// ),
// ),
//
// ],
// ),
// ),
]
// .map((e) => Padding(
// padding: const EdgeInsets.only(bottom: 50), child: e))
// .toList(),
),
],
),
),
),
);
+1 -2
View File
@@ -142,10 +142,9 @@ class _RoomPageState extends State<RoomPage> {
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: math.max(0, participants.length - 1),
itemBuilder: (BuildContext context, int index) => Container(
itemBuilder: (BuildContext context, int index) => SizedBox(
width: 100,
height: 100,
padding: const EdgeInsets.all(2),
child: ParticipantWidget(participants[index + 1],
quality: VideoQuality.LOW),
),
+30 -12
View File
@@ -7,17 +7,23 @@ import 'package:google_fonts/google_fonts.dart';
// https://github.com/flutter/flutter/issues/55092
// https://github.com/flutter/flutter/issues/39113
//
extension LKColors on Colors {
static const lkBlue = Color(0xFF5A8BFF);
static const lkDarkBlue = Color(0xFF00153C);
}
class LiveKitTheme {
//
final bgColor = Colors.black;
final textColor = Colors.white;
final cardColor = const Color(0xFF00163c);
final accentColor = const Color(0xFF2d6aef);
final cardColor = LKColors.lkDarkBlue;
final accentColor = LKColors.lkBlue;
ThemeData buildThemeData(BuildContext ctx) => ThemeData(
backgroundColor: bgColor,
// accentColor: accentColor,
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue),
// colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue),
appBarTheme: AppBarTheme(
backgroundColor: cardColor,
),
@@ -29,6 +35,14 @@ class LiveKitTheme {
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
textStyle:
MaterialStateProperty.all<TextStyle>(GoogleFonts.montserrat(
fontSize: 15,
)),
padding: MaterialStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric(vertical: 20, horizontal: 25)),
shape: MaterialStateProperty.all<OutlinedBorder>(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
// backgroundColor: MaterialStateProperty.all<Color>(accentColor),
backgroundColor: MaterialStateProperty.resolveWith((states) {
@@ -43,8 +57,13 @@ class LiveKitTheme {
checkColor: MaterialStateProperty.all(Colors.white),
fillColor: MaterialStateProperty.all(accentColor),
),
dialogBackgroundColor: cardColor,
textTheme: GoogleFonts.latoTextTheme(
dialogTheme: DialogTheme(
backgroundColor: cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
textTheme: GoogleFonts.montserratTextTheme(
Theme.of(ctx).textTheme,
).apply(
displayColor: textColor,
@@ -53,15 +72,14 @@ class LiveKitTheme {
),
hintColor: Colors.red,
inputDecorationTheme: InputDecorationTheme(
labelStyle: TextStyle(
color: textColor.withOpacity(.5),
labelStyle: const TextStyle(
color: LKColors.lkBlue,
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: textColor.withOpacity(0.1)),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: accentColor),
hintStyle: TextStyle(
color: LKColors.lkBlue.withOpacity(.5),
),
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
),
);
}
+64 -46
View File
@@ -151,56 +151,74 @@ class _ControlsWidgetState extends State<ControlsWidget> {
final videoPub = participant.videoTracks.firstOrNull;
final videoEnabled = videoPub != null && !videoPub.muted;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: _unpublishAll,
icon: const Icon(EvaIcons.closeCircleOutline),
),
if (canMute)
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 15,
horizontal: 15,
),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 5,
runSpacing: 5,
children: [
IconButton(
onPressed: _muteAudio,
icon: const Icon(EvaIcons.mic),
)
else
IconButton(
onPressed: _unmuteAudio,
icon: const Icon(EvaIcons.micOff),
onPressed: _unpublishAll,
icon: const Icon(EvaIcons.closeCircleOutline),
tooltip: 'Unpublish all',
),
if (videoEnabled)
if (canMute)
IconButton(
onPressed: _muteAudio,
icon: const Icon(EvaIcons.mic),
tooltip: 'mute audio',
)
else
IconButton(
onPressed: _unmuteAudio,
icon: const Icon(EvaIcons.micOff),
tooltip: 'un-mute audio',
),
if (videoEnabled)
IconButton(
onPressed: _muteVideo,
icon: const Icon(EvaIcons.video),
tooltip: 'mute video',
)
else
IconButton(
onPressed: _unmuteVideo,
icon: const Icon(EvaIcons.videoOff),
tooltip: 'un-mute video',
),
IconButton(
onPressed: _muteVideo,
icon: const Icon(EvaIcons.video),
)
else
IconButton(
onPressed: _unmuteVideo,
icon: const Icon(EvaIcons.videoOff),
icon: Icon(position == CameraPosition.back
? EvaIcons.camera
: EvaIcons.person),
onPressed: () => _toggleCamera(),
tooltip: 'toggle camera',
),
IconButton(
icon: Icon(position == CameraPosition.back
? EvaIcons.camera
: EvaIcons.person),
onPressed: () => _toggleCamera(),
),
IconButton(
icon: const Icon(EvaIcons.monitor),
onPressed: () => _shareScreen(),
),
IconButton(
onPressed: _onTapDisconnect,
icon: const Icon(EvaIcons.closeCircle),
),
IconButton(
onPressed: _onTapSendData,
icon: const Icon(EvaIcons.paperPlane),
),
IconButton(
onPressed: _onTapReconnect,
icon: const Icon(EvaIcons.refresh),
),
],
IconButton(
icon: const Icon(EvaIcons.monitor),
onPressed: () => _shareScreen(),
tooltip: 'share screen (experimental)',
),
IconButton(
onPressed: _onTapDisconnect,
icon: const Icon(EvaIcons.closeCircle),
tooltip: 'disconnect',
),
IconButton(
onPressed: _onTapSendData,
icon: const Icon(EvaIcons.paperPlane),
tooltip: 'send demo data',
),
IconButton(
onPressed: _onTapReconnect,
icon: const Icon(EvaIcons.refresh),
tooltip: 're-connect',
),
],
),
);
}
}
+3 -1
View File
@@ -2,6 +2,8 @@ import 'package:eva_icons_flutter/eva_icons_flutter.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:livekit_example/theme.dart';
class NoVideoWidget extends StatelessWidget {
//
const NoVideoWidget({Key? key}) : super(key: key);
@@ -12,7 +14,7 @@ class NoVideoWidget extends StatelessWidget {
child: LayoutBuilder(
builder: (ctx, constraints) => Icon(
EvaIcons.videoOffOutline,
color: Theme.of(ctx).colorScheme.secondary,
color: LKColors.lkBlue,
size: math.min(constraints.maxHeight, constraints.maxWidth) * 0.3,
),
),
+12 -1
View File
@@ -3,6 +3,7 @@ import 'package:eva_icons_flutter/eva_icons_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:livekit_example/theme.dart';
import 'no_video.dart';
import 'participant_info.dart';
@@ -64,7 +65,17 @@ class _ParticipantWidgetState extends State<ParticipantWidget> {
@override
Widget build(BuildContext ctx) => Container(
color: Theme.of(ctx).cardColor,
foregroundDecoration: BoxDecoration(
border: widget.participant.isSpeaking
? Border.all(
width: 5,
color: LKColors.lkBlue,
)
: null,
),
decoration: BoxDecoration(
color: Theme.of(ctx).cardColor,
),
child: Stack(
children: [
// Video
+48
View File
@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
class LKTextField extends StatelessWidget {
final String label;
final TextEditingController? ctrl;
const LKTextField({
required this.label,
this.ctrl,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(
label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
vertical: 15,
horizontal: 15,
),
decoration: BoxDecoration(
border: Border.all(
width: 1,
color: Colors.white.withOpacity(.3),
),
borderRadius: BorderRadius.circular(8),
),
child: TextField(
controller: ctrl,
decoration: const InputDecoration.collapsed(
hintText: '',
),
keyboardType: TextInputType.url,
),
),
],
);
}