我正在尝试通过 WebRTC 将视频从我的 golang 后端(使用此 WebRTC 实现)流式传输到客户端浏览器。
我的实现适用于 Chrome,但不适用于 Safari,因为尽管远程对等点提供了视频轨道,但
RTCPeerConnection.ontrack()
回调从未被触发。 ICE 连接似乎成功。当使用 Safari 本身作为后端对等点时,会正常调用 ontrack()
。
我见过很多人都遇到过
ontrack()
没有被呼叫的问题,但他们都没有遇到这个具体问题。
这是一个最小的可重现示例。运行它:
index.html
放入您的工作目录main.go
localhost:8080
,注意控制台中打印的 on track called!
消息ontrack()
没有被调用。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>
由于某种原因,即使您将
m=video
标记为 offerToReceiveVideo
,Safari 也不会在报价 SDP 中发送 true
。
解决方案是在将报价发送到您的 go 应用程序之前创建一个虚拟连接轨道:
function getDummyTrack() {
const canvas = document.createElement("canvas");
canvas.width = 640;
canvas.height = 480;
const ctx = canvas.getContext("2d");
// Fill the canvas with a solid color, e.g., black
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Create a stream from the canvas
const stream = canvas.captureStream(30); // 30 FPS
// Get the video track from the stream
const [videoTrack] = stream.getVideoTracks();
return videoTrack;
}
peerConnection = new RTCPeerConnection();
createDataChannel(peerConnection);
peerConnection.onicecandidate = handleIceCandidateEvent;
peerConnection.ontrack = handleTrackEvent;
peerConnection.addTrack(getDummyTrack());
const offer = await peerConnection.createOffer({
offerToReceiveVideo: true,
});
await peerConnection.setLocalDescription(offer);
signalingChannel.send(JSON.stringify({ type: "offer", data: offer.sdp }));