Add simple datachannel example with demo.html (#3252)

This PR adds a new simple -datachannel example to help newcomers
understand how to create and test a basic WebRTC DataChannel using Go
and a static HTML page (demo.html).

- Includes main.go for signaling and WebRTC setup.
- Includes demo.html to test sending/receiving messages.
- Tested locally and works with the provided signaling server.
This commit is contained in:
Sundenis
2025-10-23 09:41:52 +03:00
committed by GitHub
parent 8f7e057201
commit 919c686059
3 changed files with 260 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
# WebRTC DataChannel Example in Go
This is a minimal example of a **WebRTC DataChannel** using **Go (Pion)** as the signaling server.
## Features
- Go server for signaling
- Browser-based DataChannel
- ICE candidate exchange
- Real-time messaging between browser and Go server
## Usage
1. Run the server:
```
go run main.go
```
2. Open browser at http://localhost:8080
3. Send messages via DataChannel and see them in terminal & browser logs.

View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html>
<!--
SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
SPDX-License-Identifier: MIT
-->
<head>
<meta charset="utf-8">
<title>DataChannel Test</title>
</head>
<body>
<h2>📡 WebRTC DataChannel Test</h2>
<input id="msg" placeholder="Message">
<button id="sendBtn" disabled onclick="sendMsg()">Send</button>
<pre id="log"></pre>
<script>
const pc = new RTCPeerConnection({iceServers:[{urls:"stun:stun.l.google.com:19302"}]});
const channel = pc.createDataChannel("chat");
// Connection state monitoring
pc.onconnectionstatechange = () => log(`🔄 Connection state: ${pc.connectionState}`);
pc.oniceconnectionstatechange = () => log(`🧊 ICE state: ${pc.iceConnectionState}`);
pc.onsignalingstatechange = () => log(`📞 Signaling state: ${pc.signalingState}`);
channel.onopen = () => {
log("✅ DataChannel opened");
document.getElementById("sendBtn").disabled = false;
}
channel.onmessage = e => log(`📩 Server: ${e.data}`);
pc.onicecandidate = event => {
if(event.candidate){
fetch("/candidate", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(event.candidate),
});
}
};
async function start(){
try {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const res = await fetch("/offer", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(offer),
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const answer = await res.json();
await pc.setRemoteDescription(answer);
} catch (err) {
log(`❌ Connection failed: ${err.message}`);
console.error("Connection error:", err);
}
}
function sendMsg(){
if(channel.readyState !== "open"){
log("❌ Channel not open yet");
return;
}
const msg = document.getElementById("msg").value;
if (msg.trim()) {
channel.send(msg);
log(`You: ${msg}`);
document.getElementById("msg").value = "";
}
}
function log(msg){
document.getElementById("log").textContent+=msg+"\n";
}
start();
</script>
</body>
</html>

View File

@@ -0,0 +1,149 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
//go:build !js
// +build !js
// simple-datachannel is a simple datachannel demo that auto connects.
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/pion/webrtc/v4"
)
func main() {
var pc *webrtc.PeerConnection
setupOfferHandler(&pc)
setupCandidateHandler(&pc)
setupStaticHandler()
fmt.Println("🚀 Signaling server started on http://localhost:8080")
//nolint:gosec
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Printf("Failed to start server: %v\n", err)
}
}
func setupOfferHandler(pc **webrtc.PeerConnection) {
http.HandleFunc("/offer", func(responseWriter http.ResponseWriter, r *http.Request) {
var offer webrtc.SessionDescription
if err := json.NewDecoder(r.Body).Decode(&offer); err != nil {
http.Error(responseWriter, err.Error(), http.StatusBadRequest)
return
}
// PeerConnection with enhanced configuration for better browser compatibility
var err error
*pc, err = webrtc.NewPeerConnection(webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{URLs: []string{"stun:stun.l.google.com:19302"}},
},
BundlePolicy: webrtc.BundlePolicyBalanced,
RTCPMuxPolicy: webrtc.RTCPMuxPolicyRequire,
})
if err != nil {
http.Error(responseWriter, err.Error(), http.StatusInternalServerError)
return
}
setupICECandidateHandler(*pc)
setupDataChannelHandler(*pc)
if err := processOffer(*pc, offer, responseWriter); err != nil {
http.Error(responseWriter, err.Error(), http.StatusInternalServerError)
return
}
})
}
func setupICECandidateHandler(pc *webrtc.PeerConnection) {
pc.OnICECandidate(func(c *webrtc.ICECandidate) {
if c != nil {
fmt.Printf("🌐 New ICE candidate: %s\n", c.Address)
}
})
}
func setupDataChannelHandler(pc *webrtc.PeerConnection) {
pc.OnDataChannel(func(d *webrtc.DataChannel) {
d.OnOpen(func() {
fmt.Println("✅ DataChannel opened (Server)")
if sendErr := d.SendText("Hello from Go server 👋"); sendErr != nil {
fmt.Printf("Failed to send text: %v\n", sendErr)
}
})
d.OnMessage(func(msg webrtc.DataChannelMessage) {
fmt.Printf("📩 Received: %s\n", string(msg.Data))
})
})
}
func processOffer(
pc *webrtc.PeerConnection,
offer webrtc.SessionDescription,
responseWriter http.ResponseWriter,
) error {
// Set remote description
if err := pc.SetRemoteDescription(offer); err != nil {
return err
}
// Create answer
answer, err := pc.CreateAnswer(nil)
if err != nil {
return err
}
// Set local description
if err := pc.SetLocalDescription(answer); err != nil {
return err
}
// Wait for ICE gathering to complete before sending answer
gatherComplete := webrtc.GatheringCompletePromise(pc)
<-gatherComplete
finalAnswer := pc.LocalDescription()
if finalAnswer == nil {
//nolint:err113
return fmt.Errorf("local description is nil after ICE gathering")
}
responseWriter.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(responseWriter).Encode(*finalAnswer); err != nil {
fmt.Printf("Failed to encode answer: %v\n", err)
}
return nil
}
func setupCandidateHandler(pc **webrtc.PeerConnection) {
http.HandleFunc("/candidate", func(responseWriter http.ResponseWriter, r *http.Request) {
var candidate webrtc.ICECandidateInit
if err := json.NewDecoder(r.Body).Decode(&candidate); err != nil {
http.Error(responseWriter, err.Error(), http.StatusBadRequest)
return
}
if *pc != nil {
if err := (*pc).AddICECandidate(candidate); err != nil {
fmt.Println("Failed to add candidate", err)
}
}
})
}
func setupStaticHandler() {
// demo.html
http.HandleFunc("/", func(responseWriter http.ResponseWriter, r *http.Request) {
http.ServeFile(responseWriter, r, "./demo.html")
})
}