From 70e67827131eab89a69936bb2f92921e4a5a4c79 Mon Sep 17 00:00:00 2001 From: whm <973418690@qq.com> Date: Wed, 25 Mar 2026 16:30:48 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=B4=E6=92=AD=EF=BC=9APLI=20=E5=85=B3?= =?UTF-8?q?=E9=94=AE=E5=B8=A7=E8=AF=B7=E6=B1=82=E3=80=81=E8=A7=82=E4=BC=97?= =?UTF-8?q?=20RTCP=20=E8=AF=BB=E5=8F=96=E3=80=81=E6=96=AD=E7=BA=BF?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E9=87=8D=E8=BF=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- server/pkg/weblive/hub.go | 12 +++++++++- server/pkg/weblive/ws.go | 16 ++++++++++++++ web/src/utils/liveWebRTC.js | 44 +++++++++++++++++++++++-------------- 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/server/pkg/weblive/hub.go b/server/pkg/weblive/hub.go index 84920b5..eb8b43e 100644 --- a/server/pkg/weblive/hub.go +++ b/server/pkg/weblive/hub.go @@ -168,9 +168,19 @@ func (h *Hub) attachForwardersToViewerPC(v *viewerSession) { if err != nil { continue } - if _, err := v.pc.AddTrack(lt); err != nil { + rtpSender, err := v.pc.AddTrack(lt) + if err != nil { continue } + // Drain RTCP feedback to keep interceptors/senders healthy. + go func() { + rtcpBuf := make([]byte, 1500) + for { + if _, _, e := rtpSender.Read(rtcpBuf); e != nil { + return + } + } + }() tf.addViewer(v.id, lt) } } diff --git a/server/pkg/weblive/ws.go b/server/pkg/weblive/ws.go index 914fecb..9357abb 100644 --- a/server/pkg/weblive/ws.go +++ b/server/pkg/weblive/ws.go @@ -5,12 +5,14 @@ import ( "log" "net/http" "sync" + "time" "yh_web/server/handlers" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/gorilla/websocket" + "github.com/pion/rtcp" "github.com/pion/webrtc/v3" ) @@ -117,6 +119,20 @@ func handlePublisherWS(c *gin.Context, h *Hub) { pc.OnTrack(func(track *webrtc.TrackRemote, _ *webrtc.RTPReceiver) { log.Printf("weblive: publisher track kind=%s", track.Kind().String()) h.onPublisherTrack(track) + if track.Kind() == webrtc.RTPCodecTypeVideo { + go func(ssrc uint32) { + _ = pc.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: ssrc}}) + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + for range ticker.C { + st := pc.ConnectionState() + if st == webrtc.PeerConnectionStateClosed || st == webrtc.PeerConnectionStateFailed { + return + } + _ = pc.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: ssrc}}) + } + }(uint32(track.SSRC())) + } }) pc.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { diff --git a/web/src/utils/liveWebRTC.js b/web/src/utils/liveWebRTC.js index 2af045c..aa004d6 100644 --- a/web/src/utils/liveWebRTC.js +++ b/web/src/utils/liveWebRTC.js @@ -78,6 +78,28 @@ export function startViewing(videoEl, opts = {}) { }, 6000) } + function rebuildPeer() { + try { + pc.close() + } catch (_) {} + pc = newPeer((e) => { + if (videoEl && e.streams[0]) { + videoEl.srcObject = e.streams[0] + } + }) + } + + function resumePollingAfterDisconnect(tip) { + if (stopped) return + clearBlackFrameTimer() + if (videoEl) videoEl.srcObject = null + if (tip) onStatus(tip) + if (pollTimer) return + rebuildPeer() + pollTimer = setInterval(poll, pollMs) + poll() + } + async function negotiate() { if (stopped) return const icePending = [] @@ -118,33 +140,21 @@ export function startViewing(videoEl, opts = {}) { } catch (_) {} } if (msg.type === 'ended') { - clearBlackFrameTimer() - onStatus(msg.message || '直播已结束') - if (videoEl) videoEl.srcObject = null onEnded() try { ws?.close() } catch (_) {} ws = null - try { - pc.close() - } catch (_) {} - pc = newPeer((e) => { - if (videoEl && e.streams[0]) { - videoEl.srcObject = e.streams[0] - } - }) - if (!stopped && !pollTimer) { - pollTimer = setInterval(poll, pollMs) - poll() - } + resumePollingAfterDisconnect(msg.message || '直播已结束') } if (msg.type === 'error') { onStatus(msg.message || '错误') } } ws.onclose = () => { - if (!stopped) onStatus('连接已断开') + if (stopped) return + ws = null + resumePollingAfterDisconnect('连接已断开,正在尝试重连…') } } @@ -175,9 +185,11 @@ export function startViewing(videoEl, opts = {}) { stopped = true clearBlackFrameTimer() if (pollTimer) clearInterval(pollTimer) + pollTimer = null try { ws?.close() } catch (_) {} + ws = null pc.close() if (videoEl) videoEl.srcObject = null }