Demo App
Here is the code for a sample application made using this package.
GitHub Link:
https://github.com/Huddle-01/huddle01-flutter-example
Folder Structure
lib
┣ logic
┃ ┗ blocs
┃ ┃ ┣ consumers
┃ ┃ ┃ ┣ consumers_bloc.dart
┃ ┃ ┃ ┣ consumers_event.dart
┃ ┃ ┃ ┗ consumers_state.dart
┃ ┃ ┣ me
┃ ┃ ┃ ┣ me_bloc.dart
┃ ┃ ┃ ┣ me_event.dart
┃ ┃ ┃ ┗ me_state.dart
┃ ┃ ┣ media_devices
┃ ┃ ┃ ┣ media_devices_bloc.dart
┃ ┃ ┃ ┣ media_devices_event.dart
┃ ┃ ┃ ┗ media_devices_state.dart
┃ ┃ ┣ peers
┃ ┃ ┃ ┣ peers_bloc.dart
┃ ┃ ┃ ┣ peers_event.dart
┃ ┃ ┃ ┗ peers_state.dart
┃ ┃ ┣ producers
┃ ┃ ┃ ┣ producers_bloc.dart
┃ ┃ ┃ ┣ producers_event.dart
┃ ┃ ┃ ┗ producers_state.dart
┃ ┃ ┗ room
┃ ┃ ┃ ┣ room_bloc.dart
┃ ┃ ┃ ┣ room_event.dart
┃ ┃ ┃ ┗ room_state.dart
┣ presentation
┃ ┣ components
┃ ┃ ┣ list_media_devices
┃ ┃ ┃ ┗ list_media_devices.dart
┃ ┃ ┣ me
┃ ┃ ┃ ┣ microphone.dart
┃ ┃ ┃ ┣ renderMe.dart
┃ ┃ ┃ ┗ webcam.dart
┃ ┃ ┗ others
┃ ┃ ┃ ┣ other.dart
┃ ┃ ┃ ┗ renderOthers.dart
┃ ┣ enter_page.dart
┃ ┣ room.dart
┃ ┗ voice_audio_settings.dart
┗ main.dart
main.dart
import 'dart:developer' as dev;
import 'dart:math';
import 'package:english_words/english_words.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
import 'package:huddle01_flutter_example/logic/blocs/consumers/consumers_bloc.dart';
import 'package:huddle01_flutter_example/logic/blocs/me/me_bloc.dart';
import 'package:huddle01_flutter_example/logic/blocs/media_devices/media_devices_bloc.dart';
import 'package:huddle01_flutter_example/logic/blocs/peers/peers_bloc.dart';
import 'package:huddle01_flutter_example/logic/blocs/producers/producers_bloc.dart';
import 'package:huddle01_flutter_example/logic/blocs/room/room_bloc.dart';
import 'package:huddle01_flutter_example/presentation/enter_page.dart';
import 'package:huddle01_flutter_example/presentation/room.dart';
import 'package:random_string/random_string.dart';
import 'package:flutter/material.dart';
void main() {
runApp(BlocProvider<MediaDevicesBloc>(
create: (context) => MediaDevicesBloc()..add(MediaDeviceLoadDevices()),
lazy: false,
child: MyApp()));
}
class MyApp extends StatelessWidget {
_setupEventListeners({
required ConsumersBloc consumersBloc,
required ProducersBloc producersBloc,
required PeersBloc peersBloc,
required MeBloc meBloc,
}) {
emitter.on('addConsumer', (consumer) {
consumersBloc.add(ConsumerAdd(consumer: consumer));
});
emitter.on('removeConsumer', (consumerId) {
consumersBloc.add(ConsumerRemove(consumerId: consumerId));
});
emitter.on('addProducer', (producer) {
producersBloc.add(ProducerAdd(producer: producer));
if (producer.source == 'webcam') {
meBloc.add(MeSetWebcamInProgress(progress: true));
}
});
emitter.on('removeProducer', (source) {
producersBloc.add(ProducerRemove(source: source));
if (source == 'webcam') {
meBloc.add(MeSetWebcamInProgress(progress: false));
}
});
emitter.on('addPeer', (peer) {
peersBloc.add(PeerAdd(newPeer: peer));
});
emitter.on('removePeer', (peerId) {
peersBloc.add(PeerRemove(peerId: peerId));
});
emitter.on('addPeerConsumer', (consumer) {
peersBloc.add(
PeerAddConsumer(peerId: consumer.peerId, consumerId: consumer.id));
});
emitter.on('removePeerConsumer', (peerId, consumerId) {
peersBloc.add(PeerRemoveConsumer(peerId: peerId, consumerId: consumerId));
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
// ignore: missing_return
onGenerateRoute: (settings) {
if (settings.name == EnterPage.RoutePath) {
return MaterialPageRoute(
builder: (context) => EnterPage(),
);
}
if (settings.name == '/room') {
return MaterialPageRoute(
builder: (context) => MultiBlocProvider(
// just like multiprovider, we are providing all the blocs except the media devices one to the Room
providers: [
BlocProvider<ProducersBloc>(
lazy: false,
create: (context) => ProducersBloc(),
),
BlocProvider<ConsumersBloc>(
lazy: false,
create: (context) => ConsumersBloc(),
),
BlocProvider<PeersBloc>(
lazy: false,
create: (context) => PeersBloc(
consumersBloc: context.read<ConsumersBloc>(),
),
),
BlocProvider<MeBloc>(
lazy: false,
create: (context) => MeBloc(
displayName: nouns[Random.secure().nextInt(2500)],
id: randomAlpha(8)),
),
BlocProvider<RoomBloc>(
lazy: false,
create: (context) =>
RoomBloc(settings.arguments.toString()),
),
],
child: RepositoryProvider<HuddleClientRepository>(
// provider to provide room client as an object to all the child widgets of this
lazy: false,
create: (context) {
_setupEventListeners(
consumersBloc: context.read<ConsumersBloc>(),
producersBloc: context.read<ProducersBloc>(),
peersBloc: context.read<PeersBloc>(),
meBloc: context.read<MeBloc>(),
);
MediaDevicesBloc mediaDevicesBloc =
context.read<MediaDevicesBloc>();
String audioInputDeviceId =
mediaDevicesBloc.state.selectedAudioInput!.deviceId;
String videoInputDeviceId =
mediaDevicesBloc.state.selectedVideoInput!.deviceId;
final meState = context.read<MeBloc>().state;
String displayName = meState.displayName;
String id = meState.id;
final roomState = context.read<RoomBloc>().state;
String roomId = roomState.roomId!;
return HuddleClientRepository(
peerId: id,
displayName: displayName,
apiKey: "i4pzqbpxza8vpijQMwZsP1H7nZZEH0TN3vR4NdNS",
roomId: roomId,
audioInputDeviceId: audioInputDeviceId,
videoInputDeviceId: videoInputDeviceId,
)..join();
},
child: Room(),
)),
);
}
},
);
}
}
UI and screens
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
import 'package:huddle01_flutter_example/logic/blocs/media_devices/media_devices_bloc.dart';
import 'package:huddle01_flutter_example/logic/blocs/room/room_bloc.dart';
import 'package:huddle01_flutter_example/presentation/components/me/renderMe.dart';
import 'package:huddle01_flutter_example/presentation/components/others/renderOthers.dart';
import 'package:huddle01_flutter_example/presentation/voice_audio_settings.dart';
class Room extends StatefulWidget {
const Room({Key? key}) : super(key: key);
@override
_RoomState createState() => _RoomState();
}
class _RoomState extends State<Room> {
late StreamSubscription<MediaDevicesState> _mediaDevicesBlocSubscription;
late String audioInputDeviceId;
late String videoInputDeviceId;
@override
void dispose() {
super.dispose();
_mediaDevicesBlocSubscription.cancel();
}
@override
void initState() {
super.initState();
audioInputDeviceId =
context.read<MediaDevicesBloc>().state.selectedAudioInput!.deviceId;
videoInputDeviceId =
context.read<MediaDevicesBloc>().state.selectedVideoInput!.deviceId;
_mediaDevicesBlocSubscription = context
.read<MediaDevicesBloc>()
.stream
.listen((MediaDevicesState state) async {
if (state.selectedAudioInput != null &&
state.selectedAudioInput!.deviceId != audioInputDeviceId) {
await context.read<HuddleClientRepository>().disableMic();
context.read<HuddleClientRepository>().enableMic();
}
if (state.selectedVideoInput != null &&
state.selectedVideoInput!.deviceId != videoInputDeviceId) {
await context.read<HuddleClientRepository>().disableWebcam();
context.read<HuddleClientRepository>().enableWebcam();
}
});
}
setStreamListener(context) {}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Builder(
builder: (context) {
String roomId = context.select((RoomBloc bloc) => bloc.state.roomId!);
return Text(roomId);
},
),
actions: [
IconButton(
onPressed: () {
String roomId = context.read<RoomBloc>().state.roomId!;
Clipboard.setData(ClipboardData(text: roomId));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Room link copied to clipboard'),
duration: const Duration(seconds: 1),
),
);
},
icon: Icon(Icons.copy),
),
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: context.read<MediaDevicesBloc>(),
child: AudioVideoSettings(),
),
),
);
},
icon: Icon(Icons.settings),
),
],
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
context.read<HuddleClientRepository>().close();
Navigator.pop(context);
},
),
),
body: Stack(
fit: StackFit.expand,
children: [
RenderOther(),
RenderMe(),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
import 'package:huddle01_flutter_example/logic/blocs/media_devices/media_devices_bloc.dart';
import 'package:huddle01_flutter_example/presentation/components/list_media_devices/list_media_devices.dart';
class EnterPage extends StatefulWidget {
static const String RoutePath = '/';
@override
_EnterPageState createState() => _EnterPageState();
}
class _EnterPageState extends State<EnterPage> {
final TextEditingController _textEditingController = TextEditingController();
@override
void initState() {
super.initState();
_textEditingController.addListener(() {
final String text = _textEditingController.text.toLowerCase();
setState(() {
_textEditingController.value = _textEditingController.value.copyWith(
text: text,
);
});
});
}
@override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('mediasoup-client-flutter'),
),
body: Container(
alignment: Alignment.center,
child: Column(
children: [
TextField(
autofocus: false,
controller: _textEditingController,
decoration: InputDecoration(
border: OutlineInputBorder(),
floatingLabelBehavior: FloatingLabelBehavior.always,
hintText: 'Room url (empty = random room)',
),
),
ElevatedButton(
onPressed: () {
Navigator.pushNamed(
context,
'/room',
arguments: _textEditingController.value.text.toLowerCase(),
);
},
child: Text('Join'),
),
Text(
'Audio Input',
style: Theme.of(context).textTheme.headline5,
),
Builder(
builder: (context) {
final List<MediaDeviceInfo> audioInputDevices = context
.select((MediaDevicesBloc bloc) => bloc.state.audioInputs);
final MediaDeviceInfo? selectedAudioInput = context.select(
(MediaDevicesBloc bloc) => bloc.state.selectedAudioInput);
return ListMediaDevices(
key: Key('audioinput'),
selectedDevice: selectedAudioInput,
devices: audioInputDevices,
onSelect: (MediaDeviceInfo device) => context
.read<MediaDevicesBloc>()
.add(MediaDeviceSelectAudioInput(device)),
);
},
),
Text(
'Audio Output',
style: Theme.of(context).textTheme.headline5,
),
Builder(
builder: (context) {
final List<MediaDeviceInfo> audioOutputDevices = context
.select((MediaDevicesBloc bloc) => bloc.state.audioOutputs);
final MediaDeviceInfo? selectedAudioOutput = context.select(
(MediaDevicesBloc bloc) => bloc.state.selectedAudioOutput);
return ListMediaDevices(
key: Key('audiooutput'),
selectedDevice: selectedAudioOutput,
devices: audioOutputDevices,
onSelect: (MediaDeviceInfo device) => context
.read<MediaDevicesBloc>()
.add(MediaDeviceSelectAudioOutput(device)),
);
},
),
Text(
'Video Input',
style: Theme.of(context).textTheme.headline5,
),
Builder(
builder: (context) {
final List<MediaDeviceInfo> videoInputDevices = context
.select((MediaDevicesBloc bloc) => bloc.state.videoInputs);
final MediaDeviceInfo? selectedVideoInput = context.select(
(MediaDevicesBloc bloc) => bloc.state.selectedVideoInput);
return ListMediaDevices(
key: Key('videoinput'),
selectedDevice: selectedVideoInput,
devices: videoInputDevices,
onSelect: (MediaDeviceInfo device) => context
.read<MediaDevicesBloc>()
.add(MediaDeviceSelectVideoInput(device)),
);
},
),
],
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
import 'package:huddle01_flutter_example/logic/blocs/media_devices/media_devices_bloc.dart';
import 'package:huddle01_flutter_example/presentation/components/list_media_devices/list_media_devices.dart';
class AudioVideoSettings extends StatefulWidget {
const AudioVideoSettings({Key? key}) : super(key: key);
@override
_AudioVideoSettingsState createState() => _AudioVideoSettingsState();
}
class _AudioVideoSettingsState extends State<AudioVideoSettings> {
MediaDeviceInfo? selectedAudioInput;
MediaDeviceInfo? selectedAudioOutput;
MediaDeviceInfo? selectedVideoInput;
@override
void initState() {
super.initState();
final MediaDevicesState devicesInfo =
context.read<MediaDevicesBloc>().state;
setState(() {
selectedAudioInput = devicesInfo.selectedAudioInput;
selectedAudioOutput = devicesInfo.selectedAudioOutput;
selectedVideoInput = devicesInfo.selectedVideoInput;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Audio & Video'),
actions: [
Builder(
builder: (context) => IconButton(
onPressed: () {
final MediaDevicesState devicesInfo =
context.read<MediaDevicesBloc>().state;
final MediaDeviceInfo? audioInput =
devicesInfo.selectedAudioInput;
final MediaDeviceInfo? audioOutput =
devicesInfo.selectedAudioOutput;
final MediaDeviceInfo? videoInput =
devicesInfo.selectedVideoInput;
if (audioInput != selectedAudioInput) {
context
.read<MediaDevicesBloc>()
.add(MediaDeviceSelectAudioInput(selectedAudioInput!));
}
if (audioOutput != selectedAudioOutput) {
context
.read<MediaDevicesBloc>()
.add(MediaDeviceSelectAudioOutput(selectedAudioOutput));
}
if (videoInput != selectedVideoInput) {
context
.read<MediaDevicesBloc>()
.add(MediaDeviceSelectVideoInput(selectedVideoInput!));
}
Navigator.pop(context);
},
icon: Icon(
Icons.save,
),
),
),
],
),
body: Container(
alignment: Alignment.center,
child: Column(
children: [
Builder(
builder: (context) {
final List<MediaDeviceInfo> audioInputDevices = context
.select((MediaDevicesBloc bloc) => bloc.state.audioInputs);
return ListMediaDevices(
key: Key('audioinput'),
selectedDevice: selectedAudioInput!,
devices: audioInputDevices,
onSelect: (MediaDeviceInfo device) => setState(() {
selectedAudioInput = device;
}),
);
},
),
Text(
'Audio Output',
style: Theme.of(context).textTheme.headline5,
),
Builder(
builder: (context) {
final List<MediaDeviceInfo> audioOutputDevices = context
.select((MediaDevicesBloc bloc) => bloc.state.audioOutputs);
return ListMediaDevices(
key: Key('audiooutput'),
selectedDevice: selectedAudioOutput,
devices: audioOutputDevices,
onSelect: (MediaDeviceInfo device) => setState(() {
selectedAudioOutput = device;
}),
);
},
),
Text(
'Video Input',
style: Theme.of(context).textTheme.headline5,
),
Builder(
builder: (context) {
final List<MediaDeviceInfo> videoInputDevices = context
.select((MediaDevicesBloc bloc) => bloc.state.videoInputs);
return ListMediaDevices(
key: Key('videoinput'),
selectedDevice: selectedVideoInput!,
devices: videoInputDevices,
onSelect: (MediaDeviceInfo device) => setState(() {
selectedVideoInput = device;
}),
);
},
),
],
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
class ListMediaDevices extends StatelessWidget {
final List<MediaDeviceInfo> devices;
final MediaDeviceInfo? selectedDevice;
final Function(MediaDeviceInfo) onSelect;
const ListMediaDevices({
Key? key,
required this.devices,
this.selectedDevice,
required this.onSelect,
}) : super(key: key);
void selectDevice(int index) {
if (selectedDevice != devices[index]) {
onSelect(devices[index]);
}
}
@override
Widget build(BuildContext context) {
if (devices.isNotEmpty) {
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: devices.length,
itemBuilder: (context, index) => ListTile(
key: Key(devices[index].deviceId),
title: Text(devices[index].label),
subtitle: Text('id: ${devices[index].deviceId}'),
selected: selectedDevice == devices[index],
onTap: () => selectDevice(index),
),
);
}
return Text('No devices');
}
}
import 'package:flutter/material.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
class Other extends StatelessWidget {
final Peer peer;
final Consumer? video;
final RTCVideoRenderer? renderer;
const Other({Key? key, required this.peer, this.video, this.renderer})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 250,
width: 300,
color: Colors.black87,
child: Stack(
fit: StackFit.expand,
children: [
if (video != null && renderer != null)
RTCVideoView(renderer!)
else
Container(
height: 250,
width: 300,
decoration: BoxDecoration(color: Colors.black54),
),
Positioned(
bottom: 5,
left: 2,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
child: Text(
'${peer.displayName}\n${peer.device!.name} ${peer.device!.version}',
style: TextStyle(
color: Colors.white,
),
),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
padding: const EdgeInsets.all(8),
),
],
))
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
import 'package:huddle01_flutter_example/logic/blocs/consumers/consumers_bloc.dart';
import 'package:huddle01_flutter_example/logic/blocs/peers/peers_bloc.dart';
import 'package:huddle01_flutter_example/presentation/components/others/other.dart';
class RenderOther extends StatelessWidget {
const RenderOther({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final Map<String, Peer> peers =
context.select((PeersBloc bloc) => bloc.state.peers);
final Map<String, Consumer> consumers =
context.select((ConsumersBloc bloc) => bloc.state.consumers);
final Map<String, RTCVideoRenderer> renderers =
context.select((ConsumersBloc bloc) => bloc.state.renderers);
return GridView.count(
crossAxisCount: 2,
scrollDirection: Axis.vertical,
shrinkWrap: true,
children: peers.values.map((Peer peer) {
if (peer.consumers.isNotEmpty) {
String? id = peer.consumers.firstWhere(
(cId) => consumers[cId]?.kind == 'video',
orElse: () => 'null',
);
if (id != 'null') {
return Other(
key: Key(peer.id! + id),
peer: peer,
video: consumers[id]!,
renderer: renderers[id]!,
);
}
}
return Other(
key: Key(peer.id!),
peer: peer,
);
}).toList(),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
import 'package:huddle01_flutter_example/logic/blocs/producers/producers_bloc.dart';
class Microphone extends StatefulWidget {
const Microphone({Key? key}) : super(key: key);
@override
_MicrophoneState createState() => _MicrophoneState();
}
class _MicrophoneState extends State<Microphone> {
@override
Widget build(BuildContext context) {
return BlocBuilder<ProducersBloc, ProducersState>(
builder: (context, state) {
if (state.mic == null)
return IconButton(
icon: Icon(
Icons.mic_off,
color: Colors.grey,
),
onPressed: () {},
);
return IconButton(
onPressed: () {
if (state.mic!.paused) {
context.read<HuddleClientRepository>().unmuteMic();
setState(() {});
} else {
context.read<HuddleClientRepository>().muteMic();
setState(() {});
}
},
icon: Icon(state.mic!.paused ? Icons.mic_off : Icons.mic));
},
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
import 'package:huddle01_flutter_example/logic/blocs/me/me_bloc.dart';
import 'package:huddle01_flutter_example/logic/blocs/producers/producers_bloc.dart';
class Webcam extends StatefulWidget {
const Webcam({Key? key}) : super(key: key);
@override
_WebcamState createState() => _WebcamState();
}
class _WebcamState extends State<Webcam> {
@override
Widget build(BuildContext context) {
bool inProgress =
context.select((MeBloc bloc) => bloc.state.webcamInProgress);
return BlocBuilder<ProducersBloc, ProducersState>(
builder: (context, state) {
// if (state.webcam == null) return IconButton(icon: Icon(Icons.videocam_off, color: Colors.grey,),);
return IconButton(
onPressed: () {
// if (inProgress) {
// return;
// }
if (state.webcam != null) {
context.read<HuddleClientRepository>().disableWebcam();
setState(() {});
} else {
context.read<HuddleClientRepository>().enableWebcam();
setState(() {});
}
},
icon: Icon(
state.webcam == null ? Icons.videocam_off : Icons.videocam));
},
);
}
}
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
import 'package:huddle01_flutter_example/logic/blocs/producers/producers_bloc.dart';
import 'package:huddle01_flutter_example/presentation/components/me/microphone.dart';
import 'package:huddle01_flutter_example/presentation/components/me/webcam.dart';
class RenderMe extends StatefulWidget {
const RenderMe({Key? key}) : super(key: key);
@override
_RenderMeState createState() => _RenderMeState();
}
class _RenderMeState extends State<RenderMe> {
late RTCVideoRenderer renderer;
@override
void initState() {
super.initState();
initRenderers();
}
@override
Widget build(BuildContext context) {
try {
log('starting render me');
return BlocConsumer<ProducersBloc, ProducersState>(
listener: (context, state) {
log('inside listener');
try {
renderer.srcObject = state.webcam!.stream;
log(renderer.toString());
} catch (e) {
log(e.toString());
}
},
builder: (context, state) {
log('inside bloc consumer');
try {
return Align(
alignment: Alignment.bottomLeft,
child: Stack(
alignment: Alignment.centerRight,
children: [
Container(
width: MediaQuery.of(context).size.width * .3,
height: MediaQuery.of(context).size.height * .3,
margin: const EdgeInsets.only(left: 5, bottom: 10),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
shape: BoxShape.rectangle,
border: Border.all(
color: Colors.black,
width: 2.0,
style: BorderStyle.solid,
),
),
child: RTCVideoView(
renderer,
// mirror: true,
),
),
Positioned(
top: 5,
right: 5,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Microphone(),
Webcam(),
],
),
)
],
),
);
} catch (e) {
log(e.toString());
return Container();
}
},
);
} catch (e) {
log(e.toString());
return Container();
}
}
void initRenderers() async {
renderer = RTCVideoRenderer();
await renderer.initialize();
}
@override
void dispose() {
renderer.dispose();
super.dispose();
}
}
Logic/Blocs
room bloc
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:random_string/random_string.dart';
part 'room_event.dart';
part 'room_state.dart';
class RoomBloc extends Bloc<RoomEvent, RoomState> {
RoomBloc(String roomId)
: super(
RoomState(
roomId: roomId != null && roomId.isNotEmpty
? roomId
: randomAlpha(8).toLowerCase(),
),
);
@override
Stream<RoomState> mapEventToState(
RoomEvent event,
) async* {
if (event is RoomSetActiveSpeakerId) {
yield* _mapRoomSetActiveSpeakerIdToState(event);
}
}
Stream<RoomState> _mapRoomSetActiveSpeakerIdToState(
RoomSetActiveSpeakerId event) async* {
yield RoomState.newActiveSpeaker(state, activeSpeakerId: event.speakerId);
}
}
part of 'room_bloc.dart';
class RoomState extends Equatable {
final String? activeSpeakerId;
final String? state;
final String? roomId;
const RoomState({this.activeSpeakerId, this.state, this.roomId});
static RoomState newActiveSpeaker(
RoomState old, {
String? activeSpeakerId,
}) {
return RoomState(
roomId: old.roomId, state: old.state, activeSpeakerId: activeSpeakerId);
}
@override
List<Object> get props => [activeSpeakerId!, state!, roomId!];
}
part of 'room_bloc.dart';
abstract class RoomEvent extends Equatable {
const RoomEvent();
}
class RoomSetActiveSpeakerId extends RoomEvent {
final String speakerId;
const RoomSetActiveSpeakerId({required this.speakerId});
@override
List<Object> get props => [speakerId];
}
producers bloc
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
part 'producers_event.dart';
part 'producers_state.dart';
class ProducersBloc extends Bloc<ProducersEvent, ProducersState> {
ProducersBloc() : super(ProducersState());
@override
Stream<ProducersState> mapEventToState(
ProducersEvent event,
) async* {
if (event is ProducerAdd) {
yield* _mapProducerAddToState(event);
} else if (event is ProducerRemove) {
yield* _mapProducerRemoveToState(event);
} else if (event is ProducerResumed) {
yield* _mapProducerResumeToState(event);
} else if (event is ProducerPaused) {
yield* _mapProducerPausedToState(event);
}
}
Stream<ProducersState> _mapProducerAddToState(ProducerAdd event) async* {
switch (event.producer.source) {
case 'mic':
{
yield ProducersState.copy(state, mic: event.producer);
break;
}
case 'webcam':
{
yield ProducersState.copy(state, webcam: event.producer);
break;
}
case 'screen':
{
yield ProducersState.copy(state, screen: event.producer);
break;
}
default:
break;
}
}
Stream<ProducersState> _mapProducerRemoveToState(
ProducerRemove event) async* {
switch (event.source) {
case 'mic':
{
state.mic?.close.call();
yield ProducersState.removeMic(state);
break;
}
case 'webcam':
{
state.webcam?.close.call();
yield ProducersState.removeWebcam(state);
break;
}
case 'screen':
{
state.screen?.close.call();
yield ProducersState.removeScreen(state);
break;
}
default:
break;
}
}
Stream<ProducersState> _mapProducerResumeToState(
ProducerResumed event) async* {
switch (event.source) {
case 'mic':
{
state.mic?.resume.call();
yield ProducersState.copy(state);
break;
}
case 'webcam':
{
state.webcam?.resume.call();
yield ProducersState.copy(state);
break;
}
case 'screen':
{
state.screen?.resume.call();
yield ProducersState.copy(state);
break;
}
default:
break;
}
}
Stream<ProducersState> _mapProducerPausedToState(
ProducerPaused event) async* {
switch (event.source) {
case 'mic':
{
state.mic?.pause.call();
yield ProducersState.copy(state);
break;
}
case 'webcam':
{
state.webcam?.pause.call();
yield ProducersState.copy(state);
break;
}
case 'screen':
{
state.screen?.pause.call();
yield ProducersState.copy(state);
break;
}
default:
break;
}
}
}
part of 'producers_bloc.dart';
class ProducersState extends Equatable {
final Producer? mic;
final Producer? webcam;
final Producer? screen;
const ProducersState({
this.mic,
this.webcam,
this.screen,
});
static ProducersState copy(
ProducersState old, {
Producer? mic,
Producer? webcam,
Producer? screen,
}) {
return ProducersState(
mic: mic ?? old.mic,
webcam: webcam ?? old.webcam,
screen: screen ?? old.screen,
);
}
static ProducersState removeMic(ProducersState old) {
return ProducersState(
mic: null,
webcam: old.webcam,
screen: old.screen,
);
}
static ProducersState removeWebcam(ProducersState old) {
return ProducersState(
mic: old.mic,
webcam: null,
screen: old.screen,
);
}
static ProducersState removeScreen(ProducersState old) {
return ProducersState(
mic: old.mic,
webcam: old.webcam,
screen: null,
);
}
@override
List<Object> get props => [
if (this.mic != null) this.mic!,
if (this.webcam != null) this.webcam!,
if (this.screen != null) this.screen!
];
}
part of 'producers_bloc.dart';
abstract class ProducersEvent extends Equatable {
const ProducersEvent();
}
class ProducerAdd extends ProducersEvent {
final Producer producer;
const ProducerAdd({required this.producer});
@override
List<Object> get props => throw UnimplementedError();
}
class ProducerRemove extends ProducersEvent {
final String source;
const ProducerRemove({required this.source});
@override
List<Object> get props => [source];
}
class ProducerPaused extends ProducersEvent {
final String source;
const ProducerPaused({required this.source});
@override
List<Object> get props => [source];
}
class ProducerResumed extends ProducersEvent {
final String source;
const ProducerResumed({required this.source});
@override
List<Object> get props => [source];
}
peers bloc
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
import 'package:huddle01_flutter_example/logic/blocs/consumers/consumers_bloc.dart';
part 'peers_event.dart';
part 'peers_state.dart';
class PeersBloc extends Bloc<dynamic, PeersState> {
final ConsumersBloc consumersBloc;
PeersBloc({required this.consumersBloc}) : super(PeersState());
@override
Stream<PeersState> mapEventToState(
dynamic event,
) async* {
if (event is PeerAdd) {
yield* _mapPeerAddToState(event);
} else if (event is PeerRemove) {
yield* _mapPeerRemoveToState(event);
} else if (event is PeerAddConsumer) {
yield* _mapConsumerAddToState(event);
} else if (event is PeerRemoveConsumer) {
yield* _mapConsumerRemoveToState(event);
}
}
Stream<PeersState> _mapPeerAddToState(PeerAdd event) async* {
final Map<String, Peer> newPeers = Map<String, Peer>.of(state.peers);
final Peer newPeer = Peer.fromMap(event.newPeer);
newPeers[newPeer.id!] = newPeer;
yield PeersState(peers: newPeers);
}
Stream<PeersState> _mapPeerRemoveToState(PeerRemove event) async* {
final Map<String, Peer> newPeers = Map<String, Peer>.of(state.peers);
newPeers.remove(event.peerId);
yield PeersState(peers: newPeers);
}
Stream<PeersState> _mapConsumerAddToState(PeerAddConsumer event) async* {
final Map<String, Peer> newPeers = Map<String, Peer>.of(state.peers);
newPeers[event.peerId] = Peer.copy(newPeers[event.peerId]);
newPeers[event.peerId]!.consumers.add(event.consumerId);
yield PeersState(peers: newPeers);
}
Stream<PeersState> _mapConsumerRemoveToState(
PeerRemoveConsumer event) async* {
final Map<String, Peer> newPeers = Map<String, Peer>.of(state.peers);
newPeers[event.peerId]!
.consumers
.removeWhere
.call((c) => c == event.consumerId);
// final Map<String, Peer> newPeers = state.peers.map((key, value) {
// if (value.consumers.contains(event.consumerId)) {
// return MapEntry(key, Peer.copy(
// value,
// consumers: value
// .consumers
// .where((c) => c != event.consumerId)
// .toList(),
// ));
// }
// return MapEntry(key, value);
// });
yield PeersState(peers: newPeers);
}
}
part of 'peers_bloc.dart';
class PeersState extends Equatable {
final Map<String, Peer> peers;
const PeersState({this.peers = const <String, Peer>{}});
@override
List<Object> get props => [peers];
}
part of 'peers_bloc.dart';
abstract class PeersEvent extends Equatable {
const PeersEvent();
}
class PeerAdd extends PeersEvent {
final Map<String, dynamic> newPeer;
const PeerAdd({required this.newPeer});
@override
List<Object> get props => [newPeer];
}
class PeerAddConsumer extends PeersEvent {
final String consumerId;
final String peerId;
const PeerAddConsumer({required this.consumerId, required this.peerId});
@override
List<Object> get props => [consumerId, peerId];
}
class PeerRemoveConsumer extends PeersEvent {
final String consumerId;
final String peerId;
const PeerRemoveConsumer({required this.consumerId, required this.peerId});
@override
List<Object> get props => [consumerId, peerId];
}
class PeerRemove extends PeersEvent {
final String peerId;
const PeerRemove({required this.peerId});
@override
List<Object> get props => [peerId];
}
media devices bloc
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
part 'media_devices_event.dart';
part 'media_devices_state.dart';
class MediaDevicesBloc extends Bloc<MediaDevicesEvent, MediaDevicesState> {
MediaDevicesBloc() : super(MediaDevicesState());
@override
Stream<MediaDevicesState> mapEventToState(
MediaDevicesEvent event,
) async* {
if (event is MediaDeviceLoadDevices) {
yield* _mapLoadDevicesToState(event);
} else if (event is MediaDeviceSelectAudioInput) {
yield* _mapSelectAudioInputToState(event);
} else if (event is MediaDeviceSelectAudioOutput) {
yield* _mapSelectAudioOutputToState(event);
} else if (event is MediaDeviceSelectVideoInput) {
yield* _mapSelectVideoInputToState(event);
}
}
Stream<MediaDevicesState> _mapSelectAudioInputToState(
MediaDeviceSelectAudioInput event) async* {
yield state.copyWith(
selectedAudioInput: event.device,
);
}
Stream<MediaDevicesState> _mapSelectAudioOutputToState(
MediaDeviceSelectAudioOutput event) async* {
yield state.copyWith(
selectedAudioOutput: event.device,
);
}
Stream<MediaDevicesState> _mapSelectVideoInputToState(
MediaDeviceSelectVideoInput event) async* {
yield state.copyWith(
selectedVideoInput: event.device,
);
}
Stream<MediaDevicesState> _mapLoadDevicesToState(
MediaDeviceLoadDevices event) async* {
try {
final List<MediaDeviceInfo> devices =
await navigator.mediaDevices.enumerateDevices();
final List<MediaDeviceInfo> audioInputs = [];
final List<MediaDeviceInfo> audioOutputs = [];
final List<MediaDeviceInfo> videoInputs = [];
devices.forEach((device) {
switch (device.kind) {
case 'audioinput':
audioInputs.add(device);
break;
case 'audiooutput':
audioOutputs.add(device);
break;
case 'videoinput':
videoInputs.add(device);
break;
default:
break;
}
});
MediaDeviceInfo? selectedAudioInput;
MediaDeviceInfo? selectedAudioOutput;
MediaDeviceInfo? selectedVideoInput;
if (audioInputs.isNotEmpty) {
selectedAudioInput = audioInputs.first;
}
if (audioOutputs.isNotEmpty) {
selectedAudioOutput = audioOutputs.first;
}
if (videoInputs.isNotEmpty) {
selectedVideoInput = videoInputs.first;
}
yield MediaDevicesState(
audioInputs: audioInputs,
audioOutputs: audioOutputs,
videoInputs: videoInputs,
selectedAudioInput: selectedAudioInput,
selectedAudioOutput: selectedAudioOutput,
selectedVideoInput: selectedVideoInput,
);
} catch (e) {
log(e.toString());
}
}
}
part of 'media_devices_bloc.dart';
class MediaDevicesState extends Equatable {
final List<MediaDeviceInfo> audioInputs;
final List<MediaDeviceInfo> audioOutputs;
final List<MediaDeviceInfo> videoInputs;
final MediaDeviceInfo? selectedAudioInput;
final MediaDeviceInfo? selectedAudioOutput;
final MediaDeviceInfo? selectedVideoInput;
const MediaDevicesState({
this.audioInputs = const [],
this.audioOutputs = const [],
this.videoInputs = const [],
this.selectedAudioInput,
this.selectedAudioOutput,
this.selectedVideoInput,
});
@override
List<Object> get props => [
audioInputs,
audioOutputs,
videoInputs,
if (selectedAudioInput != null) selectedAudioInput!,
if (selectedAudioOutput != null) selectedAudioOutput!,
if (selectedVideoInput != null) selectedVideoInput!,
];
MediaDevicesState copyWith({
List<MediaDeviceInfo>? audioInputs,
List<MediaDeviceInfo>? audioOutputs,
List<MediaDeviceInfo>? videoInputs,
MediaDeviceInfo? selectedAudioInput,
MediaDeviceInfo? selectedAudioOutput,
MediaDeviceInfo? selectedVideoInput,
}) {
return MediaDevicesState(
audioInputs: audioInputs != null
? audioInputs
: List<MediaDeviceInfo>.of(this.audioInputs),
audioOutputs: audioOutputs != null
? audioOutputs
: List<MediaDeviceInfo>.of(this.audioOutputs),
videoInputs: videoInputs != null
? videoInputs
: List<MediaDeviceInfo>.of(this.videoInputs),
selectedAudioInput: selectedAudioInput != null
? selectedAudioInput
: this.selectedAudioInput,
selectedAudioOutput: selectedAudioOutput != null
? selectedAudioOutput
: this.selectedAudioOutput,
selectedVideoInput: selectedVideoInput != null
? selectedVideoInput
: this.selectedVideoInput,
);
}
}
part of 'media_devices_bloc.dart';
abstract class MediaDevicesEvent extends Equatable {
const MediaDevicesEvent();
}
class MediaDeviceLoadDevices extends MediaDevicesEvent {
@override
List<Object> get props => [];
}
class MediaDeviceSelectAudioInput extends MediaDevicesEvent {
final MediaDeviceInfo device;
const MediaDeviceSelectAudioInput(this.device);
@override
List<Object> get props => [];
}
class MediaDeviceSelectAudioOutput extends MediaDevicesEvent {
final MediaDeviceInfo? device;
const MediaDeviceSelectAudioOutput(this.device);
@override
List<Object> get props => [];
}
class MediaDeviceSelectVideoInput extends MediaDevicesEvent {
final MediaDeviceInfo device;
const MediaDeviceSelectVideoInput(this.device);
@override
List<Object> get props => [];
}
class MediaDeviceSelectVideoOut extends MediaDevicesEvent {
final MediaDeviceInfo device;
const MediaDeviceSelectVideoOut(this.device);
@override
List<Object> get props => [];
}
me bloc
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'me_event.dart';
part 'me_state.dart';
class MeBloc extends Bloc<MeEvent, MeState> {
MeBloc({required String id, required String displayName})
: super(MeState(
webcamInProgress: false,
shareInProgress: false,
id: id,
displayName: displayName,
));
@override
Stream<MeState> mapEventToState(
MeEvent event,
) async* {
if (event is MeSetWebcamInProgress) {
yield* _mapMeSetWebCamInProgressToState(event);
}
}
Stream<MeState> _mapMeSetWebCamInProgressToState(
MeSetWebcamInProgress event) async* {
yield MeState.copy(state, webcamInProgress: event.progress);
}
}
part of 'me_bloc.dart';
class MeState extends Equatable {
final String displayName;
final String id;
final bool shareInProgress;
final bool webcamInProgress;
const MeState({
required this.displayName,
required this.id,
required this.shareInProgress,
required this.webcamInProgress,
});
static MeState copy(
MeState old, {
String? displayName,
String? id,
bool? shareInProgress,
bool? webcamInProgress,
}) {
return MeState(
displayName: displayName ?? old.displayName,
id: id ?? old.id,
shareInProgress:
shareInProgress != null ? shareInProgress : old.shareInProgress,
webcamInProgress:
webcamInProgress != null ? webcamInProgress : old.webcamInProgress,
);
}
@override
List<Object> get props => [
displayName,
id,
shareInProgress,
webcamInProgress,
];
}
part of 'me_bloc.dart';
abstract class MeEvent extends Equatable {
const MeEvent();
}
class MeSetWebcamInProgress extends MeEvent {
final bool progress;
const MeSetWebcamInProgress({required this.progress});
@override
List<Object> get props => [];
}
consumers bloc
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:huddle01_flutter/huddle01_flutter.dart';
part 'consumers_event.dart';
part 'consumers_state.dart';
class ConsumersBloc extends Bloc<ConsumersEvent, ConsumersState> {
StreamController<ConsumersEvent>? subs;
ConsumersBloc() : super(ConsumersState()) {
subs = StreamController<ConsumersEvent>();
}
@override
Stream<ConsumersState> mapEventToState(
ConsumersEvent event,
) async* {
if (event is ConsumerAdd) {
yield* _mapConsumersAddToState(event);
subs?.add(event);
} else if (event is ConsumerRemove) {
yield* _mapConsumersRemoveToState(event);
subs?.add(event);
} else if (event is ConsumerResumed) {
yield* _mapConsumerResumedToState(event);
} else if (event is ConsumerPaused) {
yield* _mapConsumerPausedToState(event);
}
}
Stream<ConsumersState> _mapConsumersAddToState(ConsumerAdd event) async* {
final Map<String, Consumer> newConsumers =
Map<String, Consumer>.of(state.consumers);
final Map<String, RTCVideoRenderer> newRenderers =
Map<String, RTCVideoRenderer>.of(state.renderers);
newConsumers[event.consumer.id] = event.consumer;
if (event.consumer.kind == 'video') {
newRenderers[event.consumer.id] = RTCVideoRenderer();
await newRenderers[event.consumer.id]!.initialize();
newRenderers[event.consumer.id]!.srcObject =
newConsumers[event.consumer.id]!.stream;
}
yield ConsumersState(consumers: newConsumers, renderers: newRenderers);
}
Stream<ConsumersState> _mapConsumersRemoveToState(
ConsumerRemove event) async* {
final Map<String, Consumer> newConsumers =
Map<String, Consumer>.of(state.consumers);
final Map<String, RTCVideoRenderer> newRenderers =
Map<String, RTCVideoRenderer>.of(state.renderers);
newConsumers.remove(event.consumerId);
await newRenderers[event.consumerId]?.dispose();
newRenderers.remove(event.consumerId);
yield ConsumersState(consumers: newConsumers, renderers: newRenderers);
}
Stream<ConsumersState> _mapConsumerResumedToState(
ConsumerResumed event) async* {
final Map<String, Consumer> newConsumers =
Map<String, Consumer>.of(state.consumers);
newConsumers[event.consumerId]?.resume();
yield ConsumersState(consumers: newConsumers, renderers: state.renderers);
}
Stream<ConsumersState> _mapConsumerPausedToState(
ConsumerPaused event) async* {
final Map<String, Consumer> newConsumers =
Map<String, Consumer>.of(state.consumers);
newConsumers[event.consumerId]?.pause();
yield ConsumersState(consumers: newConsumers, renderers: state.renderers);
}
@override
Future<void> close() async {
await subs?.close();
for (var r in state.renderers.values) {
await r.dispose();
}
return super.close();
}
}
For any help, reach out to us on Slack. We are available 24*7 at: Huddle01 Community.
Last updated
Was this helpful?