直播:PLI 关键帧请求、观众 RTCP 读取、断线自动重连

Made-with: Cursor
This commit is contained in:
whm
2026-03-25 16:30:48 +08:00
parent d83a69c23a
commit 70e6782713
3 changed files with 55 additions and 17 deletions

View File

@@ -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)
}
}

View File

@@ -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) {

View File

@@ -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
}