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(),
],
),
);
}
}
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);
}
}
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;
}
}
}
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);
}
}
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());
}
}
}
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);
}
}
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?