直播:PLI 关键帧请求、观众 RTCP 读取、断线自动重连
Made-with: Cursor
This commit is contained in:
@@ -168,9 +168,19 @@ func (h *Hub) attachForwardersToViewerPC(v *viewerSession) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err := v.pc.AddTrack(lt); err != nil {
|
rtpSender, err := v.pc.AddTrack(lt)
|
||||||
|
if err != nil {
|
||||||
continue
|
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)
|
tf.addViewer(v.id, lt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"yh_web/server/handlers"
|
"yh_web/server/handlers"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/pion/rtcp"
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,6 +119,20 @@ func handlePublisherWS(c *gin.Context, h *Hub) {
|
|||||||
pc.OnTrack(func(track *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
|
pc.OnTrack(func(track *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
|
||||||
log.Printf("weblive: publisher track kind=%s", track.Kind().String())
|
log.Printf("weblive: publisher track kind=%s", track.Kind().String())
|
||||||
h.onPublisherTrack(track)
|
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) {
|
pc.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
|
||||||
|
|||||||
@@ -78,6 +78,28 @@ export function startViewing(videoEl, opts = {}) {
|
|||||||
}, 6000)
|
}, 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() {
|
async function negotiate() {
|
||||||
if (stopped) return
|
if (stopped) return
|
||||||
const icePending = []
|
const icePending = []
|
||||||
@@ -118,33 +140,21 @@ export function startViewing(videoEl, opts = {}) {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
if (msg.type === 'ended') {
|
if (msg.type === 'ended') {
|
||||||
clearBlackFrameTimer()
|
|
||||||
onStatus(msg.message || '直播已结束')
|
|
||||||
if (videoEl) videoEl.srcObject = null
|
|
||||||
onEnded()
|
onEnded()
|
||||||
try {
|
try {
|
||||||
ws?.close()
|
ws?.close()
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
ws = null
|
ws = null
|
||||||
try {
|
resumePollingAfterDisconnect(msg.message || '直播已结束')
|
||||||
pc.close()
|
|
||||||
} catch (_) {}
|
|
||||||
pc = newPeer((e) => {
|
|
||||||
if (videoEl && e.streams[0]) {
|
|
||||||
videoEl.srcObject = e.streams[0]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!stopped && !pollTimer) {
|
|
||||||
pollTimer = setInterval(poll, pollMs)
|
|
||||||
poll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (msg.type === 'error') {
|
if (msg.type === 'error') {
|
||||||
onStatus(msg.message || '错误')
|
onStatus(msg.message || '错误')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
if (!stopped) onStatus('连接已断开')
|
if (stopped) return
|
||||||
|
ws = null
|
||||||
|
resumePollingAfterDisconnect('连接已断开,正在尝试重连…')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,9 +185,11 @@ export function startViewing(videoEl, opts = {}) {
|
|||||||
stopped = true
|
stopped = true
|
||||||
clearBlackFrameTimer()
|
clearBlackFrameTimer()
|
||||||
if (pollTimer) clearInterval(pollTimer)
|
if (pollTimer) clearInterval(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
try {
|
try {
|
||||||
ws?.close()
|
ws?.close()
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
ws = null
|
||||||
pc.close()
|
pc.close()
|
||||||
if (videoEl) videoEl.srcObject = null
|
if (videoEl) videoEl.srcObject = null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user