Demo App

Here is the code for a sample application made using this package.

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