I am trying to stream video through WebRTC from my golang backend (using this WebRTC implementation) to a client browser.
My implementation works on Chrome but does not on Safari because the RTCPeerConnection.ontrack()
callback is never fired, despite the remote peer offering a video track. The ICE connection seems successful. When using Safari itself as the backend peer, ontrack()
is called normally.
I've seen many people having issues with ontrack()
not being called but none of them had this specific issue.
Here is a minimal reproducible example. To run it :
- Place
index.html
in your working directory - Run
main.go
- Visit
localhost:8080
with Chrome, notice theon track called!
message that is printed in the console - Visit the page using Safari, notice
ontrack()
is not called.
main.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"github.com/pion/webrtc/v3"
)
var localDescription webrtc.SessionDescription
func openConnection(offer *webrtc.SessionDescription) *webrtc.SessionDescription {
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
})
if err != nil {
panic(err)
}
videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion")
if err != nil {
panic(err)
}
rtpSender, err := peerConnection.AddTrack(videoTrack)
if err != nil {
panic(err)
}
go func() {
rtcpBuf := make([]byte, 1500)
for {
if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil {
return
}
}
}()
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("Connection State has changed %s \n", connectionState.String())
})
// `offer` received from browser
err = peerConnection.SetRemoteDescription(*offer)
if err != nil {
panic(err)
}
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
panic(err)
}
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
if err = peerConnection.SetLocalDescription(answer); err != nil {
panic(err)
}
<-gatherComplete
return peerConnection.LocalDescription()
}
func main() {
http.HandleFunc("/rtc", func(rw http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
rw.Header().Add("Access-Control-Allow-Origin", "*")
rw.Header().Add("Access-Control-Allow-Methods", "*")
return
}
offer := webrtc.SessionDescription{}
err := json.NewDecoder(r.Body).Decode(&offer)
if err != nil {
panic(err)
}
answer := openConnection(&offer)
err = json.NewEncoder(rw).Encode(answer)
if err != nil {
panic(err)
}
})
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
ui, err := os.ReadFile("./index.html")
if err != nil {
rw.Write([]byte("missing index.html next to main.go: " + err.Error()))
return
}
rw.Write([]byte(ui))
})
http.ListenAndServe(":8080", nil)
}
index.html
<!DOCTYPE html>
<head></head>
<body>
<script>
const pc = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302',
},
],
});
pc.oniceconnectionstatechange = (e) => console.log(pc.iceConnectionState);
pc.ontrack = (event) => console.log('on track called!');
pc.onicecandidate = async (event) => {
if (event.candidate === null) {
const response = await fetch('/rtc', {
method: 'post',
body: JSON.stringify(pc.localDescription)
})
const body = await response.json()
pc.setRemoteDescription(new RTCSessionDescription(body));
}
};
// at least one track or data channel is required for the offer to succeed
pc.createDataChannel('dummy');
pc
.createOffer({
offerToReceiveVideo: true,
offerToReceiveAudio: true,
})
.then(description => pc.setLocalDescription(description));
</script>
</body>