WebRTC直播观看端setRemoteDescription状态错误求助
WebRTC直播观看端
setRemoteDescription状态错误排查与修复 问题根源
错误DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: stable的核心原因:
- SDP类型不匹配:服务器的
handleWatch函数错误地将推流端与服务器连接生成的Answer类型SDP,标记为Offer发送给观看端。 - 状态机规则违反:观看端的PeerConnection处于
stable状态(无待处理信令),此时传入Answer类型的SDP,不符合WebRTC"先收Offer再发Answer"的信令流程。 - 架构设计缺陷:当前服务器复用推流端的PeerConnection对接所有观看端,不符合WebRTC一对一连接的模型,每个观看端需要与服务器建立独立的PeerConnection来接收媒体流。
修复方案
1. 服务器端(Go代码)修改
需要重构媒体流转发逻辑,存储推流的Track而非原PeerConnection,为每个观看端创建独立的连接:
调整结构体与全局存储
// 修改Stream结构体,存储推流的媒体Track type Stream struct { License string Tracks []*webrtc.TrackLocalStaticRTP } // 新增观看端PeerConnection存储,用于关联后续Answer处理 var ( streams = make(map[string]*Stream) watcherConnections = make(map[string]*webrtc.PeerConnection) streamsMutex sync.Mutex )
更新handlePublish函数,保存推流Track
func handlePublish(c *gin.Context) { var offerRequest OfferRequest if err := c.BindJSON(&offerRequest); err != nil { log.Println("Error decoding offer request:", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Bad Request"}) return } offer := offerRequest.SessionDescription license := offerRequest.License peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{}) if err != nil { log.Println("Error creating peer connection:", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) return } var tracks []*webrtc.TrackLocalStaticRTP // 接收推流端的Track并创建本地静态Track用于转发 peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { localTrack, err := webrtc.NewTrackLocalStaticRTP(track.Codec().RTPCodecCapability, track.ID(), track.StreamID()) if err != nil { panic(err) } tracks = append(tracks, localTrack) // 异步读取推流RTP包并转发到本地Track go func() { defer localTrack.Close() for { rtp, _, err := track.ReadRTP() if err != nil { return } if err := localTrack.WriteRTP(rtp); err != nil { return } } }() }) peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) { if candidate != nil { fmt.Println("ICE Candidate:", candidate) } }) if err = peerConnection.SetRemoteDescription(offer); err != nil { log.Println("Error setting remote description:", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) return } answer, err := peerConnection.CreateAnswer(nil) if err != nil { log.Println("Error creating answer:", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) return } if err = peerConnection.SetLocalDescription(answer); err != nil { log.Println("Error setting local description:", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) return } streamsMutex.Lock() streams[license] = &Stream{ License: license, Tracks: tracks, } streamsMutex.Unlock() c.JSON(http.StatusOK, gin.H{"answer": answer}) }
修改handleWatch函数,为观看端生成合法Offer
func handleWatch(c *gin.Context) { license := c.Param("license") streamsMutex.Lock() stream, found := streams[license] streamsMutex.Unlock() if !found { log.Println("Stream not found for license:", license) c.JSON(http.StatusNotFound, gin.H{"error": "Stream not found"}) return } // 为当前观看端创建独立的PeerConnection watcherPC, err := webrtc.NewPeerConnection(webrtc.Configuration{}) if err != nil { log.Println("Error creating watcher peer connection:", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) return } // 将推流Track添加到观看端的PeerConnection for _, track := range stream.Tracks { _, err := watcherPC.AddTrack(track) if err != nil { log.Println("Error adding track to watcher PC:", err) watcherPC.Close() c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) return } } // 创建合法的Offer并设置本地描述 offer, err := watcherPC.CreateOffer(nil) if err != nil { log.Println("Error creating offer for watcher:", err) watcherPC.Close() c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) return } if err := watcherPC.SetLocalDescription(offer); err != nil { log.Println("Error setting local description for watcher:", err) watcherPC.Close() c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) return } // 生成唯一标识关联观看端PeerConnection watcherId := fmt.Sprintf("%s-%d", license, time.Now().UnixNano()) streamsMutex.Lock() watcherConnections[watcherId] = watcherPC streamsMutex.Unlock() // 返回正确的Offer和watcherId c.JSON(http.StatusOK, gin.H{ "offer": offer, "watcherId": watcherId, }) }
更新handleAnswer函数,匹配观看端PeerConnection
func handleAnswer(c *gin.Context) { var answer webrtc.SessionDescription if err := c.BindJSON(&answer); err != nil { log.Println("Error decoding answer request:", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Bad Request"}) return } watcherId := c.Query("watcherId") if watcherId == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "watcherId is required"}) return } streamsMutex.Lock() watcherPC, found := watcherConnections[watcherId] streamsMutex.Unlock() if !found { log.Println("Watcher not found for id:", watcherId) c.JSON(http.StatusNotFound, gin.H{"error": "Watcher not found"}) return } if err := watcherPC.SetRemoteDescription(answer); err != nil { log.Println("Error setting remote description for watcher:", err) watcherPC.Close() delete(watcherConnections, watcherId) c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) return } c.JSON(http.StatusOK, gin.H{"message": "Answer processed successfully"}) }
2. 观看端(JavaScript)修改
确保每次加入创建新的PeerConnection,并携带watcherId发送Answer:
let peerConnection; const signalingServerUrl = "http://localhost:8080"; document.getElementById('joinButton').addEventListener('click', joinStream); async function joinStream() { const license = document.getElementById('licenseInput').value; if (!license) { alert("Please enter a license"); return; } try { const response = await fetch(`${signalingServerUrl}/watch/${license}`); if (!response.ok) { throw new Error('Failed to receive offer'); } const data = await response.json(); // 保存watcherId用于后续发送Answer window.watcherId = data.watcherId; await handleOffer(data.offer); } catch (error) { console.error('Error joining stream:', error); } } async function handleOffer(offer) { try { // 每次加入创建新的PeerConnection,避免状态冲突 peerConnection = new RTCPeerConnection(); peerConnection.addEventListener('icecandidate', handleICECandidate); peerConnection.addEventListener('track', handleTrack); // 设置合法的Offer类型SDP await peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); // 创建并设置Answer const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); // 发送Answer时携带watcherId const answerData = { license: document.getElementById('licenseInput').value, sdp: peerConnection.localDescription }; const response = await fetch(`${signalingServerUrl}/answer/${document.getElementById('licenseInput').value}?watcherId=${window.watcherId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(answerData) }); if (!response.ok) { throw new Error('Failed to send answer'); } console.log('Answer sent successfully'); } catch (error) { console.error('Error handling offer:', error); } } function handleICECandidate(event) { console.log('ICE candidate:', event.candidate); } function handleTrack(event) { const remoteVideo = document.getElementById('remoteVideo'); if (remoteVideo.srcObject !== event.streams[0]) { remoteVideo.srcObject = event.streams[0]; } }
关键修复点总结
- 服务器为每个观看端创建独立的PeerConnection,不再复用推流端连接
- 服务器存储推流媒体Track,通过转发实现多观看端直播
- 发送给观看端的是合法的Offer类型SDP,匹配WebRTC信令流程
- 观看端每次加入创建新PeerConnection,避免状态冲突
内容的提问来源于stack exchange,提问作者Good
相关产品推荐
相关产品推荐

