End of Life for Twilio Programmable Video - Upgrade to VideoSDKLearn More

How to Build Pion TURN WebRTC App?

Learn how to set up and manage Pion TURN with WebRTC in Go. This guide covers installation, configuration, and implementation of TURN server, signaling server, and handling multiple participants in real-time communication.

Introduction to Pion TURN Technology

What is Pion TURN?

In the world of WebRTC (Web Real-Time Communication), efficient and reliable peer-to-peer connections are paramount. However, these connections can be obstructed by NATs (Network Address Translators) and firewalls, which often block direct peer-to-peer communication. This is where TURN (Traversal Using Relays around NAT) comes into play. TURN is a protocol that aids in establishing media and data paths by relaying traffic through an intermediate server when direct connections are not possible.
Pion TURN is an open-source implementation of the TURN protocol, written in Go. It is part of the broader Pion project, which aims to provide a set of WebRTC tools and libraries for Go developers. Pion TURN stands out for its simplicity, performance, and ease of integration into Go applications. By leveraging Pion TURN, developers can facilitate seamless peer-to-peer communication in their WebRTC applications, ensuring that users experience minimal connection issues even in restrictive network environments.

Importance of Pion TURN in WebRTC

The primary goal of WebRTC is to enable real-time communication directly between browsers and devices without needing intermediaries. However, real-world networking conditions often necessitate the use of TURN servers to relay media. TURN servers act as a fallback mechanism, ensuring that WebRTC applications can function smoothly regardless of network constraints.
Pion TURN is particularly advantageous for developers working in Go due to its native implementation in the language, allowing for tight integration and optimized performance. It enables applications to handle complex networking scenarios, maintain low latency, and ensure high-quality media transmission. As a result, Pion TURN is a critical component for any robust WebRTC application, providing the reliability needed for real-time communications in challenging network conditions.
In summary, understanding and implementing Pion TURN is essential for developers aiming to build resilient WebRTC applications in Go. It addresses the common challenges of NAT traversal and firewall restrictions, enabling consistent and reliable real-time communication.

Getting Started with the Code!

In this section, we'll guide you through setting up a new Pion TURN application, from installation to project structure and architecture. By the end of this section, you'll have a basic understanding of how to set up and run a TURN server using Pion TURN in Go.

Create a New Pion TURN App

[a] Install Pion TURN

First, ensure you have Go installed on your machine. You can download and install Go from the official

Go website

.
Once you have Go installed, you can get the Pion TURN package by running the following command in your terminal:

sh

1go get github.com/pion/turn
This command fetches the Pion TURN package and adds it to your Go workspace, making it available for use in your projects.

[b] Structure of the Project

Next, let's set up the basic directory structure for our Go project. Create a new directory for your project and navigate into it:

sh

1mkdir pion-turn-app
2cd pion-turn-app
Inside this directory, create two files: main.go and go.mod.
1/pion-turn-app
2├── main.go
3└── go.mod
The main.go file will contain our main application code, and the go.mod file will define our module and manage dependencies.

App Architecture

pion-turn-webrtc
Our Pion TURN app will have a simple architecture consisting of the following components:
  • TURN Server: The core component that handles the relay of media and data.
  • Configuration: Settings that define the behavior and parameters of the TURN server.
  • Client Handling: Logic to manage connections and interactions with WebRTC clients.
This basic architecture provides a foundation to build upon, allowing us to add more features and complexity as needed.

Writing the Code

Let's start by writing the initial code to set up and run a basic TURN server.

[a] Initialize the Module

First, initialize a new Go module by running the following command in your project directory:

sh

1go mod init pion-turn-app
This command creates a go.mod file, which will look something like this:
1module pion-turn-app
2
3go 1.16
Now, open the main.go file and add the following code:

go

1package main
2
3import (
4    "github.com/pion/turn"
5    "log"
6)
7
8func main() {
9    // Initialize a new TURN server
10    s := turn.NewServer()
11    
12    // Start the TURN server on port 3478
13    err := s.Listen("0.0.0.0:3478")
14    if err != nil {
15        log.Fatalf("failed to start TURN server: %v", err)
16    }
17}
This code imports the necessary Pion TURN package, initializes a new TURN server, and starts it on port 3478. If the server fails to start, an error message will be logged.

Running the TURN Server

With the code in place, you can now run your TURN server. In your terminal, navigate to your project directory and run:

sh

1go run main.go
If everything is set up correctly, the TURN server will start and listen for incoming connections on port 3478. You should see no errors in the terminal output.
Congratulations! You've successfully set up a basic Pion TURN server using Go. In the next sections, we'll delve deeper into configuring the server and implementing additional features to handle client connections and relay media effectively.

Step 1: Get Started with main.go

In this step, we'll dive deeper into configuring our Pion TURN server. We'll add essential configurations and understand how to customize the server for different use cases.

Writing main.go

We'll start by enhancing our main.go file to include more detailed configuration options for our TURN server.

[a] Basic Setup

Here's the initial code we wrote in main.go:

go

1package main
2
3import (
4    "github.com/pion/turn"
5    "log"
6)
7
8func main() {
9    // Initialize a new TURN server
10    s := turn.NewServer()
11    
12    // Start the TURN server on port 3478
13    err := s.Listen("0.0.0.0:3478")
14    if err != nil {
15        log.Fatalf("failed to start TURN server: %v", err)
16    }
17}
This code sets up a basic TURN server that listens on all network interfaces on port 3478. Now, let's add some configurations to make our server more robust and tailored to our needs.

[b] Adding Configuration Options

We can configure the TURN server with various parameters such as realm, relay address, and authentication mechanisms. Update your main.go file to include these configurations:

go

1package main
2
3import (
4    "github.com/pion/turn"
5    "log"
6)
7
8func main() {
9    // Define TURN server configuration
10    serverConfig := turn.ServerConfig{
11        Realm: "example.com",
12        AuthHandler: func(username, realm string, srcAddr net.Addr) (string, bool) {
13            // Simple static username/password authentication
14            if username == "user" {
15                return "password", true
16            }
17            return "", false
18        },
19        ListeningPort: 3478,
20        RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
21            RelayAddress: net.ParseIP("0.0.0.0"), // Change to your server's public IP address
22            Address:      "0.0.0.0",
23        },
24    }
25
26    // Initialize a new TURN server with the configuration
27    s, err := turn.NewServer(serverConfig)
28    if err != nil {
29        log.Fatalf("failed to create TURN server: %v", err)
30    }
31
32    // Start the TURN server
33    err = s.Listen()
34    if err != nil {
35        log.Fatalf("failed to start TURN server: %v", err)
36    }
37
38    // Handle shutdown signals to gracefully stop the server
39    c := make(chan os.Signal, 1)
40    signal.Notify(c, os.Interrupt)
41    <-c
42    s.Close()
43}

Explanation of Configurations

  • Realm: A string used to define the realm for the TURN server. This is typically a domain name.
  • AuthHandler: A function that handles authentication. In this example, it checks for a static username and password. You can customize this to integrate with your authentication system.
  • ListeningPort: The port on which the TURN server listens for connections.
  • RelayAddressGenerator: This configuration defines how the server generates relay addresses. Here, we use a static IP address (which should be your server's public IP).
This setup ensures that our TURN server is secure and ready to handle real-world scenarios. The authentication handler can be enhanced to support dynamic user management, and the relay address should be set to an appropriate value based on your deployment environment.

Running the Enhanced TURN Server

With these configurations in place, you can run your enhanced TURN server using the same command as before:

sh

1go run main.go
If configured correctly, the server will start and listen on the specified port, with proper authentication and relay address settings.
This concludes Step 1. You now have a configured and running TURN server using Pion TURN in Go. In the next sections, we will cover how to wireframe all the components and handle client interactions more effectively.

Step 2: Wireframe All the Components

Now that we have our basic TURN server up and running, it's time to wireframe all the components and set up a more comprehensive structure. This involves refining our configuration, handling client interactions, and preparing the server to manage multiple peers effectively.

Configuring the TURN Server

We'll start by enhancing the configuration of our TURN server to include more detailed settings and ensure it is ready for production use.

Detailed Configuration

Update your main.go file to include a more comprehensive configuration:

go

1package main
2
3import (
4    "github.com/pion/turn"
5    "log"
6    "net"
7    "os"
8    "os/signal"
9)
10
11func main() {
12    // Define TURN server configuration
13    serverConfig := turn.ServerConfig{
14        Realm: "example.com",
15        AuthHandler: func(username, realm string, srcAddr net.Addr) (string, bool) {
16            // Simple static username/password authentication
17            if username == "user" {
18                return "password", true
19            }
20            return "", false
21        },
22        ListeningPort: 3478,
23        RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
24            RelayAddress: net.ParseIP("0.0.0.0"), // Change to your server's public IP address
25            Address:      "0.0.0.0",
26        },
27        PacketConnConfigs: []turn.PacketConnConfig{
28            {
29                PacketConn: &net.UDPConn{},
30                RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
31                    RelayAddress: net.ParseIP("0.0.0.0"), // Change to your server's public IP address
32                    Address:      "0.0.0.0",
33                },
34            },
35        },
36    }
37
38    // Initialize a new TURN server with the configuration
39    s, err := turn.NewServer(serverConfig)
40    if err != nil {
41        log.Fatalf("failed to create TURN server: %v", err)
42    }
43
44    // Start the TURN server
45    err = s.Listen()
46    if err != nil {
47        log.Fatalf("failed to start TURN server: %v", err)
48    }
49
50    // Handle shutdown signals to gracefully stop the server
51    c := make(chan os.Signal, 1)
52    signal.Notify(c, os.Interrupt)
53    <-c
54    s.Close()
55}

Explanation of Enhanced Configuration

PacketConnConfigs: This slice allows you to define multiple packet connections. Each connection can be configured with its own relay address generator and other settings. This is useful for setting up multiple interfaces or handling different types of traffic.

Handling Client Interactions

A critical aspect of a TURN server is managing client connections and relaying data efficiently. This involves setting up the server to handle multiple clients and ensuring the relay mechanism works seamlessly.

Client Management

Enhance your main.go file to handle multiple clients:

go

1package main
2
3import (
4    "github.com/pion/turn"
5    "log"
6    "net"
7    "os"
8    "os/signal"
9)
10
11func main() {
12    // Define TURN server configuration
13    serverConfig := turn.ServerConfig{
14        Realm: "example.com",
15        AuthHandler: func(username, realm string, srcAddr net.Addr) (string, bool) {
16            // Simple static username/password authentication
17            if username == "user" {
18                return "password", true
19            }
20            return "", false
21        },
22        ListeningPort: 3478,
23        RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
24            RelayAddress: net.ParseIP("0.0.0.0"), // Change to your server's public IP address
25            Address:      "0.0.0.0",
26        },
27        PacketConnConfigs: []turn.PacketConnConfig{
28            {
29                PacketConn: &net.UDPConn{},
30                RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
31                    RelayAddress: net.ParseIP("0.0.0.0"), // Change to your server's public IP address
32                    Address:      "0.0.0.0",
33                },
34            },
35        },
36    }
37
38    // Initialize a new TURN server with the configuration
39    s, err := turn.NewServer(serverConfig)
40    if err != nil {
41        log.Fatalf("failed to create TURN server: %v", err)
42    }
43
44    // Start the TURN server
45    err = s.Listen()
46    if err != nil {
47        log.Fatalf("failed to start TURN server: %v", err)
48    }
49
50    // Handle client connections
51    go func() {
52        for {
53            conn, err := s.Accept()
54            if err != nil {
55                log.Printf("failed to accept client connection: %v", err)
56                continue
57            }
58
59            go handleClient(conn)
60        }
61    }()
62
63    // Handle shutdown signals to gracefully stop the server
64    c := make(chan os.Signal, 1)
65    signal.Notify(c, os.Interrupt)
66    <-c
67    s.Close()
68}
69
70func handleClient(conn net.Conn) {
71    // Handle the client connection
72    defer conn.Close()
73    // Add logic to manage the client interaction
74    log.Printf("Client connected: %v", conn.RemoteAddr())
75}

Explanation

  • PacketConnConfigs: Added detailed packet connection configurations for handling UDP connections.
  • handleClient: A function to manage individual client connections, allowing for separate goroutines to handle each client, ensuring scalability.

Running the Enhanced TURN Server

With these enhancements, run your server using:

sh

1go run main.go
Your TURN server will now be capable of handling multiple clients, relaying media and data efficiently. In the next section, we will implement the join screen and manage WebRTC connections more effectively.

Step 3: Implement Join Screen

In this step, we'll focus on setting up the initial connection process for clients by implementing a join screen. This involves setting up the necessary WebRTC signaling and ensuring that clients can connect to the TURN server to facilitate peer-to-peer communication.

Setting up the Join Screen

To implement the join screen, we need to handle the WebRTC signaling process. This involves exchanging session descriptions and ICE candidates between peers via a signaling server. For simplicity, we'll use a basic HTTP server to handle the signaling.

Implementing the Signaling Server

We'll create a simple HTTP server in Go to manage the signaling process. This server will handle POST requests from clients to exchange session descriptions and ICE candidates.

Create a Signaling Server

Add a new file named signaling.go to your project directory:
1/pion-turn-app
2├── main.go
3├── signaling.go
4└── go.mod
In signaling.go, add the following code to set up a basic HTTP signaling server:

go

1package main
2
3import (
4    "encoding/json"
5    "log"
6    "net/http"
7    "sync"
8)
9
10type Message struct {
11    Type string `json:"type"`
12    SDP  string `json:"sdp,omitempty"`
13    ICE  string `json:"ice,omitempty"`
14}
15
16var (
17    clients   = make(map[string]chan Message)
18    clientsMu sync.Mutex
19)
20
21func signalingHandler(w http.ResponseWriter, r *http.Request) {
22    var msg Message
23    if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
24        http.Error(w, err.Error(), http.StatusBadRequest)
25        return
26    }
27
28    clientsMu.Lock()
29    defer clientsMu.Unlock()
30
31    switch msg.Type {
32    case "offer", "answer":
33        if ch, ok := clients[msg.SDP]; ok {
34            ch <- msg
35        } else {
36            clients[msg.SDP] = make(chan Message, 1)
37        }
38    case "candidate":
39        for _, ch := range clients {
40            ch <- msg
41        }
42    }
43}
44
45func main() {
46    http.HandleFunc("/signaling", signalingHandler)
47    log.Println("Signaling server is running on :8080")
48    if err := http.ListenAndServe(":8080", nil); err != nil {
49        log.Fatalf("failed to start signaling server: %v", err)
50    }
51}

Explanation of Signaling Server

  • Message Type: Defines the type of signaling message (offer, answer, or ICE candidate).
  • Clients Map: Keeps track of connected clients and their signaling channels.
  • Signaling Handler: Handles incoming signaling messages and routes them to the appropriate clients.

Integrating Signaling with TURN Server

Now that we have a signaling server, we need to integrate it with our TURN server setup. We'll update main.go to start both the TURN server and the signaling server.

Update main.go

Modify main.go to start the signaling server alongside the TURN server:

go

1package main
2
3import (
4    "github.com/pion/turn"
5    "log"
6    "net"
7    "net/http"
8    "os"
9    "os/signal"
10    "sync"
11    "encoding/json"
12)
13
14type Message struct {
15    Type string `json:"type"`
16    SDP  string `json:"sdp,omitempty"`
17    ICE  string `json:"ice,omitempty"`
18}
19
20var (
21    clients   = make(map[string]chan Message)
22    clientsMu sync.Mutex
23)
24
25func signalingHandler(w http.ResponseWriter, r *http.Request) {
26    var msg Message
27    if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
28        http.Error(w, err.Error(), http.StatusBadRequest)
29        return
30    }
31
32    clientsMu.Lock()
33    defer clientsMu.Unlock()
34
35    switch msg.Type {
36    case "offer", "answer":
37        if ch, ok := clients[msg.SDP]; ok {
38            ch <- msg
39        } else {
40            clients[msg.SDP] = make(chan Message, 1)
41        }
42    case "candidate":
43        for _, ch := range clients {
44            ch <- msg
45        }
46    }
47}
48
49func main() {
50    // Define TURN server configuration
51    serverConfig := turn.ServerConfig{
52        Realm: "example.com",
53        AuthHandler: func(username, realm string, srcAddr net.Addr) (string, bool) {
54            // Simple static username/password authentication
55            if username == "user" {
56                return "password", true
57            }
58            return "", false
59        },
60        ListeningPort: 3478,
61        RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
62            RelayAddress: net.ParseIP("0.0.0.0"), // Change to your server's public IP address
63            Address:      "0.0.0.0",
64        },
65        PacketConnConfigs: []turn.PacketConnConfig{
66            {
67                PacketConn: &net.UDPConn{},
68                RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
69                    RelayAddress: net.ParseIP("0.0.0.0"), // Change to your server's public IP address
70                    Address:      "0.0.0.0",
71                },
72            },
73        },
74    }
75
76    // Initialize a new TURN server with the configuration
77    s, err := turn.NewServer(serverConfig)
78    if err != nil {
79        log.Fatalf("failed to create TURN server: %v", err)
80    }
81
82    // Start the TURN server
83    err = s.Listen()
84    if err != nil {
85        log.Fatalf("failed to start TURN server: %v", err)
86    }
87
88    // Handle client connections
89    go func() {
90        for {
91            conn, err := s.Accept()
92            if err != nil {
93                log.Printf("failed to accept client connection: %v", err)
94                continue
95            }
96
97            go handleClient(conn)
98        }
99    }()
100
101    // Start the signaling server
102    go func() {
103        http.HandleFunc("/signaling", signalingHandler)
104        log.Println("Signaling server is running on :8080")
105        if err := http.ListenAndServe(":8080", nil); err != nil {
106            log.Fatalf("failed to start signaling server: %v", err)
107        }
108    }()
109
110    // Handle shutdown signals to gracefully stop the servers
111    c := make(chan os.Signal, 1)
112    signal.Notify(c, os.Interrupt)
113    <-c
114    s.Close()
115}
116
117func handleClient(conn net.Conn) {
118    // Handle the client connection
119    defer conn.Close()
120    // Add logic to manage the client interaction
121    log.Printf("Client connected: %v", conn.RemoteAddr())
122}

Explanation of Updates

  • Signaling Server Integration: Starts the signaling server in a separate goroutine.
  • Signaling Handler: Handles signaling messages and routes them appropriately.

Running the TURN and Signaling Server

With these updates, run your project using:

sh

1go run main.go
Your TURN server and signaling server will now be running simultaneously, allowing clients to connect, exchange signaling messages, and establish WebRTC connections using the TURN server for NAT traversal and relay.

Testing the Setup

To test your setup, you can create a simple HTML and JavaScript client that connects to the signaling server, exchanges session descriptions, and uses the TURN server for relaying media. This client would typically include:
  • A form to enter the signaling server URL.
  • Buttons to create an offer, send an answer, and add ICE candidates.
  • Event handlers to handle WebRTC peer connection events.
This concludes Step 3. You now have a basic setup to handle WebRTC signaling and TURN relay using Pion TURN in Go. In the next section, we will implement controls and further enhance our WebRTC interactions.

Step 4: Implement Controls

In this step, we'll implement controls for managing the TURN server and handling WebRTC peer connections more effectively. This includes starting, stopping, and monitoring the server, as well as managing peer connections and interactions.

Implementing Server Controls

We'll enhance our main.go file to add more control over the TURN server, including starting, stopping, and monitoring the server's status.

Adding Server Control Functions

Update main.go to include functions for starting and stopping the server, as well as handling shutdown signals gracefully.

go

1package main
2
3import (
4    "github.com/pion/turn"
5    "log"
6    "net"
7    "net/http"
8    "os"
9    "os/signal"
10    "sync"
11    "encoding/json"
12)
13
14type Message struct {
15    Type string `json:"type"`
16    SDP  string `json:"sdp,omitempty"`
17    ICE  string `json:"ice,omitempty"`
18}
19
20var (
21    clients   = make(map[string]chan Message)
22    clientsMu sync.Mutex
23)
24
25func signalingHandler(w http.ResponseWriter, r *http.Request) {
26    var msg Message
27    if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
28        http.Error(w, err.Error(), http.StatusBadRequest)
29        return
30    }
31
32    clientsMu.Lock()
33    defer clientsMu.Unlock()
34
35    switch msg.Type {
36    case "offer", "answer":
37        if ch, ok := clients[msg.SDP]; ok {
38            ch <- msg
39        } else {
40            clients[msg.SDP] = make(chan Message, 1)
41        }
42    case "candidate":
43        for _, ch := range clients {
44            ch <- msg
45        }
46    }
47}
48
49func startTURNServer() (*turn.Server, error) {
50    serverConfig := turn.ServerConfig{
51        Realm: "example.com",
52        AuthHandler: func(username, realm string, srcAddr net.Addr) (string, bool) {
53            if username == "user" {
54                return "password", true
55            }
56            return "", false
57        },
58        ListeningPort: 3478,
59        RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
60            RelayAddress: net.ParseIP("0.0.0.0"), // Change to your server's public IP address
61            Address:      "0.0.0.0",
62        },
63        PacketConnConfigs: []turn.PacketConnConfig{
64            {
65                PacketConn: &net.UDPConn{},
66                RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
67                    RelayAddress: net.ParseIP("0.0.0.0"), // Change to your server's public IP address
68                    Address:      "0.0.0.0",
69                },
70            },
71        },
72    }
73
74    s, err := turn.NewServer(serverConfig)
75    if err != nil {
76        return nil, err
77    }
78
79    go func() {
80        if err := s.Listen(); err != nil {
81            log.Fatalf("failed to start TURN server: %v", err)
82        }
83    }()
84    
85    return s, nil
86}
87
88func startSignalingServer() {
89    http.HandleFunc("/signaling", signalingHandler)
90    log.Println("Signaling server is running on :8080")
91    if err := http.ListenAndServe(":8080", nil); err != nil {
92        log.Fatalf("failed to start signaling server: %v", err)
93    }
94}
95
96func main() {
97    // Start TURN server
98    turnServer, err := startTURNServer()
99    if err != nil {
100        log.Fatalf("failed to create TURN server: %v", err)
101    }
102
103    // Start signaling server
104    go startSignalingServer()
105
106    // Handle shutdown signals to gracefully stop the servers
107    c := make(chan os.Signal, 1)
108    signal.Notify(c, os.Interrupt)
109    <-c
110    turnServer.Close()
111}
112
113func handleClient(conn net.Conn) {
114    defer conn.Close()
115    log.Printf("Client connected: %v", conn.RemoteAddr())
116}

Explanation of Functions

  • startTURNServer: Initializes and starts the TURN server with the given configuration. It runs the server in a separate goroutine to allow the main function to continue executing.
  • startSignalingServer: Starts the signaling server to handle HTTP requests for signaling. This also runs in a separate goroutine.

Managing WebRTC Peer Connections

Next, we'll enhance our WebRTC handling to manage peer connections and interactions effectively. This involves setting up event handlers for WebRTC peer connections and integrating them with our TURN and signaling servers.

Setting Up WebRTC Handlers

We'll set up the necessary WebRTC handlers to manage peer connections, ICE candidates, and media streams.

HTML and JavaScript Client

Create an HTML file (index.html) to serve as the client interface:

HTML

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>WebRTC with Pion TURN</title>
7</head>
8<body>
9    <h1>WebRTC with Pion TURN</h1>
10    <video id="localVideo" autoplay playsinline></video>
11    <video id="remoteVideo" autoplay playsinline></video>
12    <button id="startButton">Start</button>
13    <script>
14        let localStream;
15        let peerConnection;
16        const configuration = {
17            iceServers: [{
18                urls: 'turn:<TURN_SERVER_IP>:3478',
19                username: 'user',
20                credential: 'password'
21            }]
22        };
23
24        document.getElementById('startButton').addEventListener('click', async () => {
25            localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
26            document.getElementById('localVideo').srcObject = localStream;
27            
28            peerConnection = new RTCPeerConnection(configuration);
29            localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
30
31            peerConnection.onicecandidate = event => {
32                if (event.candidate) {
33                    sendMessage({ type: 'candidate', ice: event.candidate });
34                }
35            };
36
37            peerConnection.ontrack = event => {
38                document.getElementById('remoteVideo').srcObject = event.streams[0];
39            };
40
41            const offer = await peerConnection.createOffer();
42            await peerConnection.setLocalDescription(offer);
43            sendMessage({ type: 'offer', sdp: offer.sdp });
44        });
45
46        function sendMessage(message) {
47            fetch('/signaling', {
48                method: 'POST',
49                headers: { 'Content-Type': 'application/json' },
50                body: JSON.stringify(message)
51            });
52        }
53
54        async function handleMessage(message) {
55            if (message.type === 'offer') {
56                await peerConnection.setRemoteDescription(new RTCSessionDescription({ type: 'offer', sdp: message.sdp }));
57                const answer = await peerConnection.createAnswer();
58                await peerConnection.setLocalDescription(answer);
59                sendMessage({ type: 'answer', sdp: answer.sdp });
60            } else if (message.type === 'answer') {
61                await peerConnection.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: message.sdp }));
62            } else if (message.type === 'candidate') {
63                await peerConnection.addIceCandidate(new RTCIceCandidate({ candidate: message.ice }));
64            }
65        }
66    </script>
67</body>
68</html>

Explanation of HTML and JavaScript Client

  • Local Video and Remote Video Elements: These elements display the local and remote video streams.
  • Start Button: Initiates the WebRTC connection process.
  • Configuration: Specifies the TURN server details for the ICE servers configuration.
  • Event Handlers: Manages ICE candidates and media tracks, and handles incoming signaling messages.

Running the TURN and Signaling Servers with WebRTC Client

With these implementations, you can run your TURN and signaling servers and test the WebRTC client by opening index.html in a browser. Make sure to replace <TURN_SERVER_IP> with the IP address of your TURN server.
  1. Start the TURN and signaling servers:

sh

1go run main.go
  1. Open index.html in a browser and click the "Start" button to initiate the WebRTC connection.
This concludes Step 4. You now have a functional WebRTC client that interacts with the Pion TURN server and a signaling server to facilitate peer-to-peer communication. In the next sections, we will further refine the participant view and controls.

Get Free 10,000 Minutes Every Months

No credit card required to start.

Step 5: Implement Participant View

In this step, we'll enhance our WebRTC application to manage and display multiple participants. This involves handling multiple peer connections, relaying media streams, and updating the user interface to accommodate multiple video feeds.

Managing Multiple Participants

To manage multiple participants, we'll need to set up a mechanism to handle multiple peer connections and display video streams from each participant. We'll modify our signaling server and client code to support multiple connections.

Updating the Signaling Server

First, let's update our signaling server to handle multiple participants. Each participant will have a unique identifier, and we'll use this identifier to manage signaling messages for each participant.

Modifying the Signaling Server

Update signaling.go to handle multiple participants:

go

1package main
2
3import (
4    "encoding/json"
5    "log"
6    "net/http"
7    "sync"
8)
9
10type Message struct {
11    Type      string `json:"type"`
12    SDP       string `json:"sdp,omitempty"`
13    ICE       string `json:"ice,omitempty"`
14    SenderID  string `json:"sender_id"`
15    ReceiverID string `json:"receiver_id,omitempty"`
16}
17
18var (
19    clients   = make(map[string]chan Message)
20    clientsMu sync.Mutex
21)
22
23func signalingHandler(w http.ResponseWriter, r *http.Request) {
24    var msg Message
25    if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
26        http.Error(w, err.Error(), http.StatusBadRequest)
27        return
28    }
29
30    clientsMu.Lock()
31    defer clientsMu.Unlock()
32
33    if msg.ReceiverID != "" {
34        if ch, ok := clients[msg.ReceiverID]; ok {
35            ch <- msg
36        } else {
37            clients[msg.ReceiverID] = make(chan Message, 1)
38        }
39    } else {
40        for _, ch := range clients {
41            ch <- msg
42        }
43    }
44}
45
46func startSignalingServer() {
47    http.HandleFunc("/signaling", signalingHandler)
48    log.Println("Signaling server is running on :8080")
49    if err := http.ListenAndServe(":8080", nil); err != nil {
50        log.Fatalf("failed to start signaling server: %v", err)
51    }
52}
53
54func main() {
55    // Start TURN server
56    turnServer, err := startTURNServer()
57    if err != nil {
58        log.Fatalf("failed to create TURN server: %v", err)
59    }
60
61    // Start signaling server
62    go startSignalingServer()
63
64    // Handle shutdown signals to gracefully stop the servers
65    c := make(chan os.Signal, 1)
66    signal.Notify(c, os.Interrupt)
67    <-c
68    turnServer.Close()
69}
70
71func handleClient(conn net.Conn) {
72    defer conn.Close()
73    log.Printf("Client connected: %v", conn.RemoteAddr())
74}

Updating the HTML and JavaScript Client

Next, we'll update our client code to handle multiple participants. Each participant will be assigned a unique ID, and the client will manage multiple peer connections based on these IDs.

Modifying the HTML Client

Update index.html to handle multiple participants:

HTML

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>WebRTC with Pion TURN</title>
7</head>
8<body>
9    <h1>WebRTC with Pion TURN</h1>
10    <div id="videos"></div>
11    <button id="startButton">Start</button>
12    <script>
13        let localStream;
14        let peerConnections = {};
15        const configuration = {
16            iceServers: [{
17                urls: 'turn:<TURN_SERVER_IP>:3478',
18                username: 'user',
19                credential: 'password'
20            }]
21        };
22
23        document.getElementById('startButton').addEventListener('click', async () => {
24            const participantID = prompt("Enter your participant ID:");
25            localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
26
27            const localVideo = document.createElement('video');
28            localVideo.srcObject = localStream;
29            localVideo.autoplay = true;
30            localVideo.playsinline = true;
31            document.getElementById('videos').appendChild(localVideo);
32
33            sendMessage({ type: 'join', sender_id: participantID });
34
35            peerConnections[participantID] = new RTCPeerConnection(configuration);
36            localStream.getTracks().forEach(track => peerConnections[participantID].addTrack(track, localStream));
37
38            peerConnections[participantID].onicecandidate = event => {
39                if (event.candidate) {
40                    sendMessage({ type: 'candidate', ice: event.candidate, sender_id: participantID });
41                }
42            };
43
44            peerConnections[participantID].ontrack = event => {
45                const remoteVideo = document.createElement('video');
46                remoteVideo.srcObject = event.streams[0];
47                remoteVideo.autoplay = true;
48                remoteVideo.playsinline = true;
49                document.getElementById('videos').appendChild(remoteVideo);
50            };
51
52            const offer = await peerConnections[participantID].createOffer();
53            await peerConnections[participantID].setLocalDescription(offer);
54            sendMessage({ type: 'offer', sdp: offer.sdp, sender_id: participantID });
55        });
56
57        function sendMessage(message) {
58            fetch('/signaling', {
59                method: 'POST',
60                headers: { 'Content-Type': 'application/json' },
61                body: JSON.stringify(message)
62            });
63        }
64
65        async function handleMessage(message) {
66            const participantID = message.sender_id;
67
68            if (message.type === 'offer') {
69                if (!peerConnections[participantID]) {
70                    peerConnections[participantID] = new RTCPeerConnection(configuration);
71                    localStream.getTracks().forEach(track => peerConnections[participantID].addTrack(track, localStream));
72
73                    peerConnections[participantID].onicecandidate = event => {
74                        if (event.candidate) {
75                            sendMessage({ type: 'candidate', ice: event.candidate, sender_id: participantID });
76                        }
77                    };
78
79                    peerConnections[participantID].ontrack = event => {
80                        const remoteVideo = document.createElement('video');
81                        remoteVideo.srcObject = event.streams[0];
82                        remoteVideo.autoplay = true;
83                        remoteVideo.playsinline = true;
84                        document.getElementById('videos').appendChild(remoteVideo);
85                    };
86                }
87
88                await peerConnections[participantID].setRemoteDescription(new RTCSessionDescription({ type: 'offer', sdp: message.sdp }));
89                const answer = await peerConnections[participantID].createAnswer();
90                await peerConnections[participantID].setLocalDescription(answer);
91                sendMessage({ type: 'answer', sdp: answer.sdp, sender_id: participantID });
92            } else if (message.type === 'answer') {
93                await peerConnections[participantID].setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: message.sdp }));
94            } else if (message.type === 'candidate') {
95                await peerConnections[participantID].addIceCandidate(new RTCIceCandidate({ candidate: message.ice }));
96            }
97        }
98
99        async function fetchMessages() {
100            const response = await fetch('/signaling');
101            const messages = await response.json();
102
103            for (const message of messages) {
104                await handleMessage(message);
105            }
106
107            setTimeout(fetchMessages, 1000);
108        }
109
110        fetchMessages();
111    </script>
112</body>
113</html>

Explanation of HTML and JavaScript Client

  • Unique Participant ID: Each participant is prompted to enter a unique ID, which is used to manage their peer connections.
  • Peer Connections Object: Stores peer connections for each participant.
  • Dynamic Video Elements: Creates and appends video elements for each local and remote stream.
  • Signaling Messages: Manages signaling messages with unique sender and receiver IDs to handle multiple participants.

Running the TURN and Signaling Servers with Multiple Participants

With these updates, you can run your TURN and signaling servers and test the WebRTC client by opening index.html in multiple browser windows or devices. Each participant will enter a unique ID and establish peer connections with other participants.
  1. Start the TURN and signaling servers:

sh

1go run main.go
  1. Open index.html in multiple browser windows or devices, and enter a unique participant ID for each.
This concludes Step 5. You now have a WebRTC application that can handle multiple participants using Pion TURN and a signaling server. In the next section, we will implement controls to manage these connections more effectively.

Step 6: Run Your Code Now

In this final step, we'll run our complete setup and test the application to ensure everything works smoothly. We'll walk through the steps to run the TURN server, the signaling server, and the WebRTC client.

Running the TURN Server and Signaling Server

First, ensure that your TURN server and signaling server are set up correctly. We’ll run these servers concurrently to handle WebRTC connections and signaling messages.

Starting the TURN Server

  1. Open a terminal and navigate to your project directory.
  2. Run the TURN server using the following command:

sh

1go run main.go
The TURN server should start and listen on the specified port, ready to relay media and data.

Starting the Signaling Server

The signaling server is already integrated with the TURN server in main.go, so it will start automatically when you run the above command. Ensure it’s running on port 8080.

Setting Up and Running the WebRTC Client

Next, we'll set up the WebRTC client. This involves opening the index.html file in multiple browser windows or devices to simulate multiple participants.

Opening the WebRTC Client

  1. Open the index.html file in a web browser. This file contains the necessary HTML and JavaScript to handle WebRTC connections.
  2. When prompted, enter a unique participant ID for each browser window or device. This ID helps manage peer connections.

Establishing Connections

  1. Click the "Start" button to initiate the WebRTC connection process.
  2. The local video stream should appear, and the client will send signaling messages to establish connections with other participants.

Testing the Application

To test the application thoroughly, follow these steps:
  1. Multiple Participants: Open index.html in multiple browser windows or devices and enter different participant IDs.
  2. Connection Establishment: Ensure that each participant can see their local video stream and that remote video streams from other participants appear correctly.
  3. Signaling and Media Relay: Verify that signaling messages are exchanged correctly and that the TURN server relays media streams as expected.

Troubleshooting Common Issues

Here are some common issues you might encounter and their solutions:
  1. TURN Server Not Starting: Ensure that the server's configuration is correct and that the specified port is not in use.
  2. Signaling Issues: Check the signaling server's logs for errors and ensure that messages are being routed correctly between participants.
  3. ICE Candidate Problems: Verify that ICE candidates are being exchanged and added correctly. Ensure that the TURN server's public IP address is correctly configured.

Conclusion

In this comprehensive guide, we’ve walked through setting up a Pion TURN server with WebRTC in Go, implementing signaling, handling multiple participants, and testing the complete setup. This robust solution ensures seamless peer-to-peer communication even in restrictive network environments.

Want to level-up your learning? Subscribe now

Subscribe to our newsletter for more tech based insights

FAQ