Overview
Pion WebRTC can be compiled to WebAssembly (WASM) and run in web browsers. This allows you to write both client and server code in Go, using the same API on both sides.
When compiled to WASM, Pion WebRTC acts as a wrapper around the browser’s native WebRTC implementation. Not all features available in the native Go version are available in WASM.
Building for WebAssembly
Basic Build
# Build for WebAssembly
GOOS = js GOARCH = wasm go build -o main.wasm main.go
Copy WASM Exec
You need the wasm_exec.js file from your Go installation:
cp "$( go env GOROOT)/misc/wasm/wasm_exec.js" .
HTML Setup
Create an HTML file to load your WASM module:
<! DOCTYPE html >
< html >
< head >
< meta charset = "utf-8" >
< title > Pion WebRTC WASM </ title >
< script src = "wasm_exec.js" ></ script >
< script >
const go = new Go ();
WebAssembly . instantiateStreaming (
fetch ( "main.wasm" ),
go . importObject
). then (( result ) => {
go . run ( result . instance );
});
</ script >
</ head >
< body >
< h1 > Pion WebRTC in WebAssembly </ h1 >
< div id = "output" ></ div >
</ body >
</ html >
Basic WASM Example
//go:build js && wasm
// +build js,wasm
package main
import (
" fmt "
" syscall/js "
" github.com/pion/webrtc/v4 "
)
func main () {
// This runs in the browser
fmt . Println ( "Pion WebRTC WASM initialized" )
// Create PeerConnection
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 ()
// Set up handlers
peerConnection . OnICEConnectionStateChange ( func ( state webrtc . ICEConnectionState ) {
fmt . Printf ( "ICE Connection State: %s \n " , state )
})
// Keep the program running
select {}
}
When writing code for WebAssembly, use build tags:
//go:build js && wasm
// +build js,wasm
package main
// This code only compiles for WASM
For native Go code:
//go:build !js
// +build !js
package main
// This code compiles for all platforms except WASM
From api_js.go:4-5 .
API Differences
The WASM API is more limited than the native Go API:
Available in WASM
PeerConnection Creation
NewPeerConnection works the same way
Basic Configuration
ICE servers and basic configuration options
Data Channels
Create and use data channels, including detaching
Event Handlers
OnICECandidate, OnConnectionStateChange, etc.
Not Available in WASM
These features are only available in native Go builds:
MediaEngine configuration
InterceptorRegistry
Most SettingEngine options
Custom network interfaces
UDP/TCP mux
Custom logging (browser console is used)
SettingEngine in WASM
The WASM SettingEngine has limited functionality:
//go:build js && wasm
package main
import " github.com/pion/webrtc/v4 "
func main () {
s := webrtc . SettingEngine {}
// Only data channel detaching is supported
s . DetachDataChannels ()
api := webrtc . NewAPI ( webrtc . WithSettingEngine ( s ))
peerConnection , _ := api . NewPeerConnection ( webrtc . Configuration {})
defer peerConnection . Close ()
}
From settingengine_js.go:9-24 .
Data Channel Example
//go:build js && wasm
package main
import (
" fmt "
" syscall/js "
" github.com/pion/webrtc/v4 "
)
func main () {
// Create PeerConnection
peerConnection , err := webrtc . NewPeerConnection ( webrtc . Configuration {})
if err != nil {
panic ( err )
}
defer peerConnection . Close ()
// Create data channel
dataChannel , err := peerConnection . CreateDataChannel ( "data" , nil )
if err != nil {
panic ( err )
}
// Handle data channel open
dataChannel . OnOpen ( func () {
fmt . Println ( "Data channel opened" )
// Send a message
err := dataChannel . SendText ( "Hello from WASM!" )
if err != nil {
fmt . Printf ( "Send error: %v \n " , err )
}
})
// Handle incoming messages
dataChannel . OnMessage ( func ( msg webrtc . DataChannelMessage ) {
fmt . Printf ( "Received: %s \n " , string ( msg . Data ))
})
// Handle data channel close
dataChannel . OnClose ( func () {
fmt . Println ( "Data channel closed" )
})
select {}
}
Detached Data Channels
Detach data channels for direct I/O access:
//go:build js && wasm
package main
import (
" fmt "
" io "
" github.com/pion/webrtc/v4 "
)
func main () {
s := webrtc . SettingEngine {}
s . DetachDataChannels ()
api := webrtc . NewAPI ( webrtc . WithSettingEngine ( s ))
peerConnection , _ := api . NewPeerConnection ( webrtc . Configuration {})
defer peerConnection . Close ()
dataChannel , _ := peerConnection . CreateDataChannel ( "data" , nil )
dataChannel . OnOpen ( func () {
fmt . Println ( "Data channel opened, detaching..." )
// Detach the data channel
raw , err := dataChannel . Detach ()
if err != nil {
panic ( err )
}
// Now you have a ReadWriteCloser
go func () {
buf := make ([] byte , 1024 )
for {
n , err := raw . Read ( buf )
if err != nil {
if err != io . EOF {
fmt . Printf ( "Read error: %v \n " , err )
}
return
}
fmt . Printf ( "Read %d bytes: %s \n " , n , string ( buf [: n ]))
}
}()
// Write data
raw . Write ([] byte ( "Hello from detached channel" ))
})
select {}
}
Interacting with JavaScript
You can interact with JavaScript from your WASM code:
//go:build js && wasm
package main
import (
" fmt "
" syscall/js "
" github.com/pion/webrtc/v4 "
)
func updateUI ( message string ) {
// Get DOM element
document := js . Global (). Get ( "document" )
output := document . Call ( "getElementById" , "output" )
// Update content
output . Set ( "innerHTML" , message )
}
func main () {
// Expose Go function to JavaScript
js . Global (). Set ( "createConnection" , js . FuncOf ( func ( this js . Value , args [] js . Value ) interface {} {
go func () {
peerConnection , err := webrtc . NewPeerConnection ( webrtc . Configuration {})
if err != nil {
updateUI ( fmt . Sprintf ( "Error: %v " , err ))
return
}
defer peerConnection . Close ()
peerConnection . OnICEConnectionStateChange ( func ( state webrtc . ICEConnectionState ) {
updateUI ( fmt . Sprintf ( "ICE State: %s " , state ))
})
updateUI ( "PeerConnection created!" )
}()
return nil
}))
updateUI ( "WASM loaded - call createConnection() from JavaScript" )
select {}
}
In your HTML:
< button onclick = " createConnection ()" > Create Connection </ button >
< div id = "output" ></ div >
Serving WASM
You need a web server to serve WASM files:
package main
import (
" log "
" net/http "
)
func main () {
// Serve current directory
fs := http . FileServer ( http . Dir ( "." ))
http . Handle ( "/" , fs )
log . Println ( "Serving on http://localhost:8080" )
log . Fatal ( http . ListenAndServe ( ":8080" , nil ))
}
Run the server:
Then open http://localhost:8080 in your browser.
Build Script
Create a build script for convenience:
#!/bin/bash
echo "Building WASM..."
GOOS = js GOARCH = wasm go build -o main.wasm main.go
echo "Copying wasm_exec.js..."
cp "$( go env GOROOT)/misc/wasm/wasm_exec.js" .
echo "Build complete!"
echo "Run: go run server.go"
Debugging
Browser Console
All fmt.Println calls appear in the browser console:
fmt . Println ( "This appears in browser console" )
fmt . Printf ( "ICE State: %s \n " , state )
Source Maps
Build with debug information:
GOOS = js GOARCH = wasm go build -gcflags= "all=-N -l" -o main.wasm main.go
Check WASM Support
//go:build js && wasm
package main
import (
" fmt "
" syscall/js "
)
func main () {
fmt . Println ( "WASM is supported!" )
fmt . Printf ( "User Agent: %s \n " , js . Global (). Get ( "navigator" ). Get ( "userAgent" ). String ())
fmt . Printf ( "Go version: %s \n " , js . Global (). Get ( "Go" ). Get ( "version" ). String ())
}
Limitations
Be aware of these limitations when using WASM:
No Media Engine : Can’t configure codecs directly
No Interceptors : Can’t intercept RTP/RTCP packets
Limited SettingEngine : Only data channel detaching supported
Browser Restrictions : Subject to browser WebRTC policies
Performance : WASM may be slower than native code
Bundle Size : WASM files can be large (several MB)
Best Practices
Use Build Tags
Separate WASM and native code with build tags
Minimize WASM Size
Only include necessary code in WASM builds
Handle Errors Gracefully
WASM errors appear in browser console - make them informative
Test in Multiple Browsers
Different browsers may behave differently
Complete Example
main.go: //go:build js && wasm
package main
import (
" fmt "
" syscall/js "
" github.com/pion/webrtc/v4 "
)
func main () {
fmt . Println ( "Pion WebRTC WASM Example" )
js . Global (). Set ( "startWebRTC" , js . FuncOf ( startWebRTC ))
select {}
}
func startWebRTC ( this js . Value , args [] js . Value ) interface {} {
go func () {
config := webrtc . Configuration {
ICEServers : [] webrtc . ICEServer {
{ URLs : [] string { "stun:stun.l.google.com:19302" }},
},
}
pc , err := webrtc . NewPeerConnection ( config )
if err != nil {
fmt . Printf ( "Error: %v \n " , err )
return
}
defer pc . Close ()
dc , _ := pc . CreateDataChannel ( "test" , nil )
dc . OnOpen ( func () {
fmt . Println ( "Data channel opened!" )
dc . SendText ( "Hello from WASM!" )
})
dc . OnMessage ( func ( msg webrtc . DataChannelMessage ) {
fmt . Printf ( "Received: %s \n " , string ( msg . Data ))
})
pc . OnICEConnectionStateChange ( func ( state webrtc . ICEConnectionState ) {
fmt . Printf ( "ICE State: %s \n " , state )
})
}()
return nil
}
index.html: <! DOCTYPE html >
< html >
< head >
< meta charset = "utf-8" >
< title > Pion WASM Example </ title >
< script src = "wasm_exec.js" ></ script >
< script >
const go = new Go ();
WebAssembly . instantiateStreaming (
fetch ( "main.wasm" ),
go . importObject
). then (( result ) => {
go . run ( result . instance );
});
</ script >
</ head >
< body >
< h1 > Pion WebRTC WASM </ h1 >
< button onclick = " startWebRTC ()" > Start WebRTC </ button >
< p > Check console for output </ p >
</ body >
</ html >
Build and run: GOOS = js GOARCH = wasm go build -o main.wasm main.go
cp "$( go env GOROOT)/misc/wasm/wasm_exec.js" .
go run server.go
SettingEngine Limited settings available in WASM
Data Channels Data channel usage guide