3

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 the on 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>
Arthur Chaloin
  • 610
  • 1
  • 5
  • 12
  • Have the same with 15.2, however on 14.x and 16.x (or maybe just different computers) it seems to work fine. Did you manage to sort it out? – Alexey Kureev Oct 28 '22 at 10:04

0 Answers0