mirror of
https://github.com/pion/webrtc.git
synced 2025-12-24 11:51:03 +08:00
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:
23
examples/data-channels-simple/README.md
Normal file
23
examples/data-channels-simple/README.md
Normal 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.
|
||||
|
||||
88
examples/data-channels-simple/demo.html
Normal file
88
examples/data-channels-simple/demo.html
Normal 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>
|
||||
149
examples/data-channels-simple/main.go
Normal file
149
examples/data-channels-simple/main.go
Normal 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")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user