Skip to main content
The data-channels example demonstrates how to establish a WebRTC connection and exchange arbitrary data between a web browser and a Pion WebRTC server using DataChannels. This is the foundation for building chat applications, game state synchronization, file transfers, and more.

Overview

DataChannels provide a bidirectional peer-to-peer communication channel that can send arbitrary data. Unlike media tracks which are optimized for real-time audio/video, DataChannels are flexible and can be configured for different reliability and ordering requirements.

Key Features

  • Automatic DataChannel handling from browser
  • Bidirectional message exchange
  • Random message generation every 5 seconds
  • Connection state monitoring
  • Base64 session description encoding for easy copy-paste signaling

How It Works

1

Create PeerConnection

Initialize a new RTCPeerConnection with STUN server configuration:
config := webrtc.Configuration{
    ICEServers: []webrtc.ICEServer{
        {
            URLs: []string{"stun:stun.l.google.com:19302"},
        },
    },
}

peerConnection, err := webrtc.NewPeerConnection(config)
if err != nil {
    panic(err)
}
defer peerConnection.Close()
2

Register DataChannel Handler

Set up a handler that fires when the browser creates a DataChannel:
peerConnection.OnDataChannel(func(dataChannel *webrtc.DataChannel) {
    fmt.Printf("New DataChannel %s %d\n", dataChannel.Label(), dataChannel.ID())
    
    // Register handlers for this channel
    dataChannel.OnOpen(func() {
        // Channel is ready for communication
    })
    
    dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
        fmt.Printf("Message from DataChannel '%s': '%s'\n", 
            dataChannel.Label(), string(msg.Data))
    })
})
3

Exchange Session Descriptions

Perform WebRTC handshake by exchanging offer and answer:
// Receive offer from browser
offer := webrtc.SessionDescription{}
decode(readUntilNewline(), &offer)

err = peerConnection.SetRemoteDescription(offer)
if err != nil {
    panic(err)
}

// Create and send answer
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
    panic(err)
}

err = peerConnection.SetLocalDescription(answer)
if err != nil {
    panic(err)
}

// Wait for ICE gathering to complete
<-webrtc.GatheringCompletePromise(peerConnection)

// Output answer to paste in browser
fmt.Println(encode(peerConnection.LocalDescription()))
4

Send Messages

Once the channel is open, send periodic messages:
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()

for range ticker.C {
    message, err := randutil.GenerateCryptoRandomString(
        15, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
    )
    if err != nil {
        panic(err)
    }
    
    fmt.Printf("Sending '%s'\n", message)
    if err = dataChannel.SendText(message); err != nil {
        panic(err)
    }
}

Complete Source Code

package main

import (
    "bufio"
    "encoding/base64"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "os"
    "strings"
    "time"

    "github.com/pion/randutil"
    "github.com/pion/webrtc/v4"
)

func main() {
    // Prepare the configuration
    config := webrtc.Configuration{
        ICEServers: []webrtc.ICEServer{
            {
                URLs: []string{"stun:stun.l.google.com:19302"},
            },
        },
    }

    // Create a new RTCPeerConnection
    peerConnection, err := webrtc.NewPeerConnection(config)
    if err != nil {
        panic(err)
    }
    defer func() {
        if cErr := peerConnection.Close(); cErr != nil {
            fmt.Printf("cannot close peerConnection: %v\n", cErr)
        }
    }()

    // Set the handler for Peer connection state
    peerConnection.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
        fmt.Printf("Peer Connection State has changed: %s\n", state.String())

        if state == webrtc.PeerConnectionStateFailed {
            fmt.Println("Peer Connection has gone to failed exiting")
            os.Exit(0)
        }

        if state == webrtc.PeerConnectionStateClosed {
            fmt.Println("Peer Connection has gone to closed exiting")
            os.Exit(0)
        }
    })

    // Register data channel creation handling
    peerConnection.OnDataChannel(func(dataChannel *webrtc.DataChannel) {
        fmt.Printf("New DataChannel %s %d\n", dataChannel.Label(), dataChannel.ID())

        // Register channel opening handling
        dataChannel.OnOpen(func() {
            fmt.Printf(
                "Data channel '%s'-'%d' open. Random messages will now be sent every 5 seconds\n",
                dataChannel.Label(), dataChannel.ID(),
            )

            ticker := time.NewTicker(5 * time.Second)
            defer ticker.Stop()
            for range ticker.C {
                message, sendErr := randutil.GenerateCryptoRandomString(
                    15, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
                )
                if sendErr != nil {
                    panic(sendErr)
                }

                // Send the message as text
                fmt.Printf("Sending '%s'\n", message)
                if sendErr = dataChannel.SendText(message); sendErr != nil {
                    panic(sendErr)
                }
            }
        })

        // Register text message handling
        dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
            fmt.Printf("Message from DataChannel '%s': '%s'\n", 
                dataChannel.Label(), string(msg.Data))
        })
    })

    // Wait for the offer to be pasted
    offer := webrtc.SessionDescription{}
    decode(readUntilNewline(), &offer)

    // Set the remote SessionDescription
    err = peerConnection.SetRemoteDescription(offer)
    if err != nil {
        panic(err)
    }

    // Create an answer
    answer, err := peerConnection.CreateAnswer(nil)
    if err != nil {
        panic(err)
    }

    // Create channel that is blocked until ICE Gathering is complete
    gatherComplete := webrtc.GatheringCompletePromise(peerConnection)

    // Sets the LocalDescription, and starts our UDP listeners
    err = peerConnection.SetLocalDescription(answer)
    if err != nil {
        panic(err)
    }

    // Block until ICE Gathering is complete
    <-gatherComplete

    // Output the answer in base64 so we can paste it in browser
    fmt.Println(encode(peerConnection.LocalDescription()))

    // Block forever
    select {}
}

Important Concepts

The example monitors PeerConnectionState rather than ICEConnectionState:
  • PeerConnectionStateFailed: No network activity for 30 seconds or another failure occurred
  • PeerConnectionStateDisconnected: Faster timeout detection, but connection may recover
  • PeerConnectionStateClosed: Explicit closure, usually from DTLS CloseNotify
Choose the appropriate state based on your reconnection strategy.
This example disables Trickle ICE for simplicity:
// Wait for all ICE candidates to be gathered
<-webrtc.GatheringCompletePromise(peerConnection)
In production, use Trickle ICE to allow ICE gathering and connection establishment to happen concurrently:
peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
    if candidate != nil {
        // Send candidate to remote peer via signaling
    }
})
By default, DataChannels are reliable and ordered (like TCP). You can configure them differently:
// Unreliable, unordered (like UDP)
dataChannel, err := peerConnection.CreateDataChannel("unreliable", &webrtc.DataChannelInit{
    Ordered: &[]bool{false}[0],
    MaxRetransmits: &[]uint16{0}[0],
})
This is useful for real-time gaming or other latency-sensitive applications.

Running the Example

1

Start the application

cd examples/data-channels
go run main.go
2

Open in browser

Navigate to the examples server at http://localhost and select the Data Channels example
3

Complete handshake

  1. Click to generate an offer in the browser
  2. Copy the offer and paste it into the terminal
  3. Copy the answer from the terminal
  4. Paste the answer back into the browser
4

Exchange messages

  • The server will send random messages every 5 seconds
  • Type messages in the browser to send them to the server
  • Watch both the browser console and terminal for messages
DataChannels support both text and binary data. Use SendText() for strings and Send() for binary data ([]byte).

Use Cases

Chat Applications

Build real-time messaging with low latency using DataChannels

File Transfer

Send files peer-to-peer without going through a server

Game State Sync

Synchronize game state between players in real-time

Remote Control

Send control commands to IoT devices or remote applications

Build docs developers (and LLMs) love