直播后台:新增按 IP 发言管控与在线会话视图。

支持全体禁言、单 IP 禁言/解禁与同 IP 本地限频,同时在开播页展示在线会话明细并补充实时优先码率策略,兼顾实时性与并发承载。

Made-with: Cursor
This commit is contained in:
whm
2026-03-26 17:57:50 +08:00
parent 0da93fb1be
commit fe8d5a34cc
8 changed files with 535 additions and 2 deletions

View File

@@ -37,6 +37,20 @@ const QUALITY_MEDIA = {
}
}
// 码率上限kbps: 在实时性与并发之间取平衡
const QUALITY_VIDEO_MAX_KBPS = {
source: 1800,
high: 1400,
mid: 900,
low: 550
}
const BITRATE_PROFILE_MULTIPLIER = {
save: 0.78,
balanced: 1,
clarity: 1.2
}
function effectivePublishQualityKey() {
try {
const v = localStorage.getItem(LIVE_CAPTURE_QUALITY_STORAGE_KEY)
@@ -45,6 +59,27 @@ function effectivePublishQualityKey() {
return 'source'
}
function targetVideoMaxBitrateBps(publishKey, bitrateProfile = 'balanced') {
const kbps = QUALITY_VIDEO_MAX_KBPS[publishKey] || QUALITY_VIDEO_MAX_KBPS.source
const m = BITRATE_PROFILE_MULTIPLIER[bitrateProfile] || BITRATE_PROFILE_MULTIPLIER.balanced
return Math.max(220, Math.round(kbps * m)) * 1000
}
async function applyVideoSenderPolicy(sender, publishKey, bitrateProfile) {
if (!sender) return
try {
const p = sender.getParameters ? sender.getParameters() : null
if (!p) return
if (!p.encodings || !p.encodings.length) p.encodings = [{}]
p.degradationPreference = 'maintain-framerate'
p.encodings[0].maxBitrate = targetVideoMaxBitrateBps(publishKey, bitrateProfile)
// 保留一定冗余,弱网抖动时更稳,避免一路拉满
p.encodings[0].maxFramerate =
publishKey === 'low' ? 20 : publishKey === 'mid' ? 24 : 30
await sender.setParameters(p)
} catch (_) {}
}
function liveWsURLPublish(token) {
const q = effectivePublishQualityKey()
const path = `/api/web/live/ws?role=publish&token=${encodeURIComponent(token)}&quality=${encodeURIComponent(q)}`
@@ -132,6 +167,7 @@ export function startPublishing(opts = {}) {
token = '',
captureMode: initialMode = 'camera',
videoDeviceId: initialDeviceId = '',
bitrateProfile = 'balanced',
onStatus = () => {},
onLocalStream = () => {},
onActiveModeChange = () => {},
@@ -245,6 +281,9 @@ export function startPublishing(opts = {}) {
throw e
}
const previewVid = new MediaStream([sTr])
try {
sTr.contentHint = 'detail'
} catch (_) {}
onLocalStream({ layout: 'screen_only', main: previewVid })
return new MediaStream([sTr, ...micStream.getAudioTracks()])
}
@@ -309,6 +348,9 @@ export function startPublishing(opts = {}) {
cam.getTracks().forEach((t) => t.stop())
throw new Error('画布采集失败')
}
try {
outV.contentHint = 'detail'
} catch (_) {}
const mic = cam.getAudioTracks()
const publish = new MediaStream([outV, ...mic])
onLocalStream({
@@ -319,6 +361,10 @@ export function startPublishing(opts = {}) {
return publish
}
const s = await navigator.mediaDevices.getUserMedia(buildCameraConstraints(publishKey, deviceIdState))
try {
const camV = s.getVideoTracks()[0]
if (camV) camV.contentHint = 'motion'
} catch (_) {}
onLocalStream({ layout: 'camera', main: s })
return s
}
@@ -359,6 +405,11 @@ export function startPublishing(opts = {}) {
stream.getTracks().forEach((t) => {
if (t.readyState === 'live') pc.addTrack(t, stream)
})
await applyVideoSenderPolicy(
pc.getSenders().find((s) => s.track?.kind === 'video'),
publishKey,
bitrateProfile
)
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
send({ type: 'offer', sdp: offer.sdp })
@@ -410,6 +461,11 @@ export function startPublishing(opts = {}) {
} else if (aT) {
pc.addTrack(aT, stream)
}
await applyVideoSenderPolicy(
pc.getSenders().find((s) => s.track?.kind === 'video'),
publishKey,
bitrateProfile
)
const offer = await pc.createOffer({ iceRestart: false })
await pc.setLocalDescription(offer)
send({ type: 'offer', sdp: offer.sdp })