直播后台:新增按 IP 发言管控与在线会话视图。
支持全体禁言、单 IP 禁言/解禁与同 IP 本地限频,同时在开播页展示在线会话明细并补充实时优先码率策略,兼顾实时性与并发承载。 Made-with: Cursor
This commit is contained in:
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user