Simplified Peer to Peer Communication with PeerDart
Tags: flutter, webrtc, websocket, dart
Published: 2022-10-16 22:34:13

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.

```bash
flutter create peerdart-example
```

2. Add PeerDart and flutter-webrtc as dependencies.

```bash 
flutter pub add peerdart flutter-webrtc
```

Step 2 — Implementation

1- Initiliaze PeerDart

```dart
final Peer peer = Peer()
```

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.

```dart
peer.on("open").listen((event) => print("Connection established"))
```


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

```dart
peer.on<DataConnection>("connection").listen((data) {
  setState(() {
    connected = true;
  });
})
```

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

```dart
peer.on("data").listen((data) {
  ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(data)));
})
```

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.

```dart
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)));
    });
  });
}
```


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

```dart
void sendHelloWorld() {
  conn.send("Hello world!");
}
```


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.

```dart
final _localRenderer = RTCVideoRenderer();
final _remoteRenderer = RTCVideoRenderer();
bool inCall = false;
```


Step — 2 Initialize video renderers inside initState.

```dart
@override
void initState() {
  super.initState();
  _localRenderer.initialize();
  _remoteRenderer.initialize();
}
```


Step — 3 Adding additional listeners for receiving calls.

```dart
 @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;
        });
      });
    });
  }
```

Step — 4 Calling other peer with call() function

```dart
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;
    });
  });
}
```


Step — 5 Finalizing Things

```dart
@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),
      ),
    );
  }
```

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.