Skip to content

Commit 404c7f7

Browse files
fippoSean-Der
authored andcommitted
Add WARP example
1 parent 0425062 commit 404c7f7

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed

examples/warp/index.html

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<!--
4+
SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
5+
SPDX-License-Identifier: MIT
6+
-->
7+
<head>
8+
<meta charset="utf-8">
9+
<title>WARP: Faster WebRTC with SNAP and SPED</title>
10+
</head>
11+
<body>
12+
<h2>📡 WebRTC DataChannel Test</h2>
13+
<div>
14+
<label><input type="checkbox" id="dtlsRole" />Act as DTLS client</label>
15+
<button id="startBtn" onclick="start()">Start</button>
16+
</div>
17+
<input id="msg" placeholder="Message">
18+
<button id="sendBtn" disabled onclick="sendMsg()">Send</button>
19+
<pre id="log"></pre>
20+
21+
<script>
22+
const pc = new RTCPeerConnection();
23+
const channel = pc.createDataChannel("chat");
24+
25+
pc.onconnectionstatechange = async () => {
26+
log(`🔄 Connection state: ${pc.connectionState}`);
27+
if (pc.connectionState === 'connected') {
28+
const stats = await pc.getStats();
29+
const transport = [...stats.values()].find(o => o.type === 'transport');
30+
if (transport) {
31+
log(`DTLS role: ${transport.dtlsRole}`);
32+
const pair = stats.get(transport.selectedCandidatePairId);
33+
if (pair) {
34+
log(`RTT: ${pair.totalRoundTripTime / pair.responsesReceived}`);
35+
}
36+
}
37+
}
38+
}
39+
pc.oniceconnectionstatechange = () => log(`🧊 ICE state: ${pc.iceConnectionState}`);
40+
pc.onsignalingstatechange = () => log(`📞 Signaling state: ${pc.signalingState}`);
41+
42+
channel.onopen = () => {
43+
log("✅ DataChannel opened");
44+
document.getElementById("sendBtn").disabled = false;
45+
}
46+
channel.onmessage = e => log(`📩 Server: ${e.data}`);
47+
48+
pc.onicecandidate = event => {
49+
if(event.candidate){
50+
fetch("/candidate", {
51+
method: "POST",
52+
headers: {"Content-Type": "application/json"},
53+
body: JSON.stringify(event.candidate),
54+
});
55+
}
56+
};
57+
pc.ondatachannel = event => {
58+
log("Server opened a channel", event.channel.name)
59+
event.channel.onmessage = (ev) => {
60+
log(`Server sent: ${ev.data}`)
61+
}
62+
};
63+
64+
async function start(){
65+
document.getElementById("startBtn").disabled = true;
66+
document.getElementById("dtlsRole").disabled = true;
67+
try {
68+
await pc.setLocalDescription();
69+
const offer = pc.localDescription;
70+
if (offer.sdp.indexOf("\na=sctp-init:") !== -1) {
71+
log("✅ sctp-init found in offer, SNAP is supported");
72+
}
73+
74+
const sdp = document.getElementById("dtlsRole").checked
75+
? offer.sdp.replace("actpass", "active")
76+
: offer.sdp;
77+
// TODO: parameters to disable SPED/SNAP?
78+
const res = await fetch("/offer", {
79+
method: "POST",
80+
headers: {"Content-Type": "application/json"},
81+
body: JSON.stringify({type: "offer", sdp}),
82+
})
83+
84+
if (!res.ok) {
85+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
86+
}
87+
88+
const answer = await res.json();
89+
if (answer.sdp.indexOf("\na=sctp-init:") !== -1) {
90+
log("✅ sctp-init found in answer, SNAP is supported");
91+
}
92+
await pc.setRemoteDescription(answer);
93+
94+
} catch (err) {
95+
log(`❌ Connection failed: ${err.message}`);
96+
console.error("Connection error:", err);
97+
}
98+
}
99+
100+
function sendMsg(){
101+
const msg = document.getElementById("msg").value;
102+
103+
if (msg.trim()) {
104+
channel.send(msg);
105+
log(`You: ${msg}`);
106+
document.getElementById("msg").value = "";
107+
}
108+
}
109+
110+
function log(msg){
111+
document.getElementById("log").textContent+=msg+"\n";
112+
}
113+
114+
115+
</script>
116+
</body>
117+
</html>

examples/warp/main.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
//go:build !js
5+
// +build !js
6+
7+
// WARP (SNAP+SPED) testbed.
8+
package main
9+
10+
import (
11+
"encoding/json"
12+
"fmt"
13+
"net/http"
14+
15+
"github.com/pion/webrtc/v4"
16+
)
17+
18+
func main() {
19+
var pc *webrtc.PeerConnection
20+
21+
setupOfferHandler(&pc)
22+
setupCandidateHandler(&pc)
23+
setupStaticHandler()
24+
25+
fmt.Println("google-chrome-unstable --force-fieldtrials=" +
26+
"WebRTC-Sctp-Snap/Enabled/WebRTC-IceHandshakeDtls/Enabled/ " +
27+
"--disable-features=WebRtcPqcForDtls http://localhost:8080")
28+
fmt.Printf("Add `--enable-logging --v=1` and then " +
29+
"`grep SCTP_PACKET chrome_debug.log | " +
30+
"text2pcap -D -u 1001,2001 -t \"%%H:%%M:%%S.%%f\" - out.pcap` " +
31+
"for inspecting the raw packets.\n")
32+
fmt.Println("🚀 Signaling server started on http://localhost:8080")
33+
//nolint:gosec
34+
if err := http.ListenAndServe(":8080", nil); err != nil {
35+
fmt.Printf("Failed to start server: %v\n", err)
36+
}
37+
}
38+
39+
func setupOfferHandler(pc **webrtc.PeerConnection) {
40+
http.HandleFunc("/offer", func(responseWriter http.ResponseWriter, r *http.Request) {
41+
var offer webrtc.SessionDescription
42+
if err := json.NewDecoder(r.Body).Decode(&offer); err != nil {
43+
http.Error(responseWriter, err.Error(), http.StatusBadRequest)
44+
45+
return
46+
}
47+
48+
var err error
49+
*pc, err = webrtc.NewPeerConnection(webrtc.Configuration{
50+
BundlePolicy: webrtc.BundlePolicyMaxBundle,
51+
})
52+
if err != nil {
53+
http.Error(responseWriter, err.Error(), http.StatusInternalServerError)
54+
55+
return
56+
}
57+
58+
setupICECandidateHandler(*pc)
59+
setupDataChannelHandler(*pc)
60+
61+
if err := processOffer(*pc, offer, responseWriter); err != nil {
62+
http.Error(responseWriter, err.Error(), http.StatusInternalServerError)
63+
64+
return
65+
}
66+
})
67+
}
68+
69+
func setupICECandidateHandler(pc *webrtc.PeerConnection) {
70+
pc.OnICECandidate(func(c *webrtc.ICECandidate) {
71+
if c != nil {
72+
fmt.Printf("🌐 New ICE candidate: %s\n", c.Address)
73+
}
74+
})
75+
}
76+
77+
func setupDataChannelHandler(pc *webrtc.PeerConnection) {
78+
pc.OnDataChannel(func(d *webrtc.DataChannel) {
79+
d.OnOpen(func() {
80+
fmt.Println("✅ DataChannel opened (Server)")
81+
if sendErr := d.SendText("Hello from Go server 👋"); sendErr != nil {
82+
fmt.Printf("Failed to send text: %v\n", sendErr)
83+
}
84+
})
85+
d.OnMessage(func(msg webrtc.DataChannelMessage) {
86+
fmt.Printf("📩 Received: %s\n", string(msg.Data))
87+
if sendErr := d.SendText("ECHO " + string(msg.Data)); sendErr != nil {
88+
fmt.Printf("Failed to send text: %v\n", sendErr)
89+
}
90+
})
91+
})
92+
if serverDc, err := pc.CreateDataChannel("server-opened-channel", nil); err == nil {
93+
serverDc.OnOpen(func() {
94+
if sendErr := serverDc.SendText("Server opened channel ready"); sendErr != nil {
95+
fmt.Printf("Failed to send on server-opened channel: %v\n", sendErr)
96+
}
97+
})
98+
}
99+
}
100+
101+
func processOffer(
102+
pc *webrtc.PeerConnection,
103+
offer webrtc.SessionDescription,
104+
responseWriter http.ResponseWriter,
105+
) error {
106+
// Set remote description
107+
if err := pc.SetRemoteDescription(offer); err != nil {
108+
return err
109+
}
110+
111+
// Create answer
112+
answer, err := pc.CreateAnswer(nil)
113+
if err != nil {
114+
return err
115+
}
116+
117+
// Set local description
118+
if err := pc.SetLocalDescription(answer); err != nil {
119+
return err
120+
}
121+
122+
// Wait for ICE gathering to complete before sending answer
123+
gatherComplete := webrtc.GatheringCompletePromise(pc)
124+
<-gatherComplete
125+
126+
finalAnswer := pc.LocalDescription()
127+
if finalAnswer == nil {
128+
//nolint:err113
129+
return fmt.Errorf("local description is nil after ICE gathering")
130+
}
131+
132+
responseWriter.Header().Set("Content-Type", "application/json")
133+
if err := json.NewEncoder(responseWriter).Encode(*finalAnswer); err != nil {
134+
fmt.Printf("Failed to encode answer: %v\n", err)
135+
}
136+
137+
return nil
138+
}
139+
140+
func setupCandidateHandler(pc **webrtc.PeerConnection) {
141+
http.HandleFunc("/candidate", func(responseWriter http.ResponseWriter, r *http.Request) {
142+
var candidate webrtc.ICECandidateInit
143+
if err := json.NewDecoder(r.Body).Decode(&candidate); err != nil {
144+
http.Error(responseWriter, err.Error(), http.StatusBadRequest)
145+
146+
return
147+
}
148+
if *pc != nil {
149+
if err := (*pc).AddICECandidate(candidate); err != nil {
150+
fmt.Println("Failed to add candidate", err)
151+
}
152+
}
153+
})
154+
}
155+
156+
func setupStaticHandler() {
157+
http.HandleFunc("/", func(responseWriter http.ResponseWriter, r *http.Request) {
158+
http.ServeFile(responseWriter, r, "./index.html")
159+
})
160+
}

0 commit comments

Comments
 (0)