DEV Community

Muhammed Kaplan
Muhammed Kaplan

Posted on • Originally published at Medium

Simplified Peer to Peer Communication with PeerDart

PeerDart is an easy-to-use wrapper for the WebRTC API. Stream data or media in real-time between clients using peer-to-peer communication

In this tutorial, we will create a basic video chat app using PeerDart that allows two clients to connect and stream video and audio with ease.

How PeerDart Simplifies WebRTC?
When it comes to real-time P2P communication in applications, WebRTC is the standard used by many developers. But, it comes with some complexities as follows;

If you use pure WebRTC, first, you define a STUN (Session Traversal Utilities for NAT) server to generate ICE (Interactive Connectivity Establishment) candidates for each peer involved in communication.
Then you need to use your servers to store these ICE candidate details.
Finally, you need to implement WebSockets to handle real-time updates.
Even you haven’t worked with WebRTC before; I’m sure you must be feeling the complexity of its implementation. But, don’t worry, PeerDart is here for the rescue.

With PeerDart, we don’t have to worry about STUNs, ICE candidates, or server creation. We can even avoid implementing WebSockets as well.

PeerDart provides a complete, configurable peer-to-peer connection API and a server called PeerServer to easily establish connections between PeerDart clients.

So, let’s see how we can use PeerDart to create a simple chat/video application.

Getting Setup
Step 1 — Create a new Flutter project
First, create a Flutter project as you would do normally.

flutter create peerdart-example
Enter fullscreen mode Exit fullscreen mode
  1. Add PeerDart and flutter-webrtc as dependencies.
flutter pub add peerdart flutter-webrtc
Enter fullscreen mode Exit fullscreen mode

Step 2 — Implementation

1- Initiliaze PeerDart

final Peer peer = Peer()
Enter fullscreen mode Exit fullscreen mode

Tip: You can assign your own id with id property.

final Peer peer = Peer(id: “custom id”)

Peer instance has several methods to handle communication between peers. peer.on is used to listen to peer events, and it is useful when receiving calls/data from remote peers.

open event will be emitted after successfully connecting to PeerServer, and we will use this event to update the state of peerId and peer instance.

peer.on("open").listen((event) => print("Connection established"))
Enter fullscreen mode Exit fullscreen mode

Then, we need to use the connection event to listen to remote peer connections.

peer.on<DataConnection>("connection").listen((data) {
  setState(() {
    connected = true;
  });
})
Enter fullscreen mode Exit fullscreen mode

Then, for listening to data we received from another peer:

peer.on("data").listen((data) {
  ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(data)));
})
Enter fullscreen mode Exit fullscreen mode

Now, we have implemented all the functionalities to receive messages. As the final step, let’s create a method to send a message.

peer.connect method allows us to connect to the peer by specifying peer id. Then it returns a DataConnection object which can be used to send message data to the peer.

void connect() {
  final connection = peer.connect(_controller.text);
  conn = connection;

  conn.on("open").listen((event) {
    setState(() {
      connected = true;
    });

    // Peer closed
    connection.on("close").listen((event) {
      setState(() {
        connected = false;
      });
    });

    conn.on("data").listen((data) {
      ScaffoldMessenger.of(context)
          .showSnackBar(SnackBar(content: Text(data)));
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

And for sending message to other peer simply use send() function to send.

void sendHelloWorld() {
  conn.send("Hello world!");
}
Enter fullscreen mode Exit fullscreen mode

Step 3 — Video Chat Implementation
Now, let’s modify our chat room to send video messages. Implementing it is pretty much similar to what we discussed in the previous step. We can use the call event inside peer.on method to listen to calls from the remote peer. It will provide a callback with an object named MediaConnection and receiver’s video and audio streams are provided to the answer method of MediaConnection object.

Step — 1 Adding additional state for rendering video instances.

final _localRenderer = RTCVideoRenderer();
final _remoteRenderer = RTCVideoRenderer();
bool inCall = false;
Enter fullscreen mode Exit fullscreen mode

Step — 2 Initialize video renderers inside initState.

@override
void initState() {
  super.initState();
  _localRenderer.initialize();
  _remoteRenderer.initialize();
}
Enter fullscreen mode Exit fullscreen mode

Step — 3 Adding additional listeners for receiving calls.

 @override
  void initState() {
    super.initState();
    //.....

    peer.on<MediaConnection>("call").listen((call) async {
      final mediaStream = await navigator.mediaDevices
          .getUserMedia({"video": true, "audio": false});

      call.answer(mediaStream);

      call.on("close").listen((event) {
        setState(() {
          inCall = false;
        });
      });

      call.on<MediaStream>("stream").listen((event) {
        _localRenderer.srcObject = mediaStream;
        _remoteRenderer.srcObject = event;

        setState(() {
          inCall = true;
        });
      });
    });
  }
Enter fullscreen mode Exit fullscreen mode

Step — 4 Calling other peer with call() function

void connect() async {
  final mediaStream = await navigator.mediaDevices
      .getUserMedia({"video": true, "audio": false});

  final conn = peer.call(_controller.text, mediaStream);

  conn.on("close").listen((event) {
    setState(() {
      inCall = false;
    });
  });

  conn.on<MediaStream>("stream").listen((event) {
    _remoteRenderer.srcObject = event;
    _localRenderer.srcObject = mediaStream;

    setState(() {
      inCall = true;
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Step — 5 Finalizing Things

@override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              _renderState(),
              const Text(
                'Connection ID:',
              ),
              SelectableText(peerId ?? ""),
              TextField(
                controller: _controller,
              ),
              ElevatedButton(onPressed: connect, child: const Text("connect")),
              ElevatedButton(
                  onPressed: send, child: const Text("send message")),
              if (inCall)
                Expanded(
                  child: RTCVideoView(
                    _localRenderer,
                  ),
                ),
              if (inCall)
                Expanded(
                  child: RTCVideoView(
                    _remoteRenderer,
                  ),
                ),
            ],
          ),
        ));
  }

  Widget _renderState() {
    Color bgColor = inCall ? Colors.green : Colors.grey;
    Color txtColor = Colors.white;
    String txt = inCall ? "Connected" : "Standby";
    return Container(
      decoration: BoxDecoration(color: bgColor),
      child: Text(
        txt,
        style:
            Theme.of(context).textTheme.titleLarge?.copyWith(color: txtColor),
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

That’s it! Now we are all set for a quick video chat. The final implementation will look like this and you can find the full code in examples here.

Top comments (2)

Collapse
 
harunkurtdev profile image
harunkurtdev

can you send github link ?

Collapse
 
hamidabdulmalik profile image
Hamid Abdulmalik Al-Hassan

This is a very wonderful project. Hope I could find some time and start contributing.