直播:画质可选、只读 /live/info、弹幕 WS 透传;Nginx 弹幕路径
Made-with: Cursor
This commit is contained in:
@@ -3,8 +3,44 @@
|
||||
*/
|
||||
const apiBase = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, '')
|
||||
|
||||
function liveWsURLPublish(token) {
|
||||
const path = `/api/web/live/ws?role=publish&token=${encodeURIComponent(token)}`
|
||||
export const LIVE_QUALITY_OPTIONS = [
|
||||
{ value: 'source', label: '原画(设备默认)' },
|
||||
{ value: 'high', label: '高清 720p' },
|
||||
{ value: 'mid', label: '标清 480p' },
|
||||
{ value: 'low', label: '流畅 360p' }
|
||||
]
|
||||
|
||||
const QUALITY_MEDIA = {
|
||||
source: { video: true, audio: true },
|
||||
high: {
|
||||
video: {
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
frameRate: { ideal: 30 }
|
||||
},
|
||||
audio: true
|
||||
},
|
||||
mid: {
|
||||
video: {
|
||||
width: { ideal: 854 },
|
||||
height: { ideal: 480 },
|
||||
frameRate: { ideal: 24 }
|
||||
},
|
||||
audio: true
|
||||
},
|
||||
low: {
|
||||
video: {
|
||||
width: { ideal: 640 },
|
||||
height: { ideal: 360 },
|
||||
frameRate: { ideal: 20 }
|
||||
},
|
||||
audio: true
|
||||
}
|
||||
}
|
||||
|
||||
function liveWsURLPublish(token, quality) {
|
||||
const q = QUALITY_MEDIA[quality] ? quality : 'high'
|
||||
const path = `/api/web/live/ws?role=publish&token=${encodeURIComponent(token)}&quality=${encodeURIComponent(q)}`
|
||||
if (apiBase) {
|
||||
const base = apiBase.replace(/\/$/, '')
|
||||
const wsOrigin = base.replace(/^https:/i, 'wss:').replace(/^http:/i, 'ws:')
|
||||
@@ -48,15 +84,21 @@ function humanizeGetUserMediaError(err) {
|
||||
* @param {string} opts.token 管理员 JWT
|
||||
* @param {(s: string) => void} [opts.onStatus]
|
||||
* @param {(stream: MediaStream) => void} [opts.onLocalStream]
|
||||
* @param {'source'|'high'|'mid'|'low'} [opts.quality] 推流画质(约束摄像头采集分辨率)
|
||||
*/
|
||||
export function startPublishing(opts = {}) {
|
||||
const { token = '', onStatus = () => {}, onLocalStream = () => {} } = opts
|
||||
const {
|
||||
token = '',
|
||||
quality = 'high',
|
||||
onStatus = () => {},
|
||||
onLocalStream = () => {}
|
||||
} = opts
|
||||
if (!token) {
|
||||
onStatus('未登录,无法开播')
|
||||
return { stop: () => {} }
|
||||
}
|
||||
|
||||
const wsUrl = liveWsURLPublish(token)
|
||||
const wsUrl = liveWsURLPublish(token, quality)
|
||||
const ws = new WebSocket(wsUrl)
|
||||
const pc = new RTCPeerConnection({ iceServers: defaultIce })
|
||||
let stream = null
|
||||
@@ -75,7 +117,8 @@ export function startPublishing(opts = {}) {
|
||||
onStatus('信令已连接,正在采集摄像头…')
|
||||
try {
|
||||
// 后台多为 PC:不要用 facingMode:'user',部分机器会直接导致 “Could not start video source”
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
const cons = QUALITY_MEDIA[quality] || QUALITY_MEDIA.high
|
||||
stream = await navigator.mediaDevices.getUserMedia(cons)
|
||||
onLocalStream(stream)
|
||||
stream.getTracks().forEach((t) => pc.addTrack(t, stream))
|
||||
const offer = await pc.createOffer()
|
||||
|
||||
@@ -10,6 +10,17 @@
|
||||
<code>LIVE_PUBLIC_IP</code>(服务器公网 IPv4,与域名一致),并配置 <code>LIVE_ICE_SERVERS</code>(含 TURN)。
|
||||
</p>
|
||||
<p class="status">{{ status }}</p>
|
||||
<div v-if="!session" class="quality-row">
|
||||
<span class="quality-label">推流画质</span>
|
||||
<el-select v-model="quality" style="width: 220px" :disabled="!token">
|
||||
<el-option
|
||||
v-for="o in qualityOptions"
|
||||
:key="o.value"
|
||||
:label="o.label"
|
||||
:value="o.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button v-if="!session" type="primary" :disabled="!token" @click="start">开始直播</el-button>
|
||||
<el-button v-else type="danger" @click="stop">结束直播</el-button>
|
||||
@@ -23,7 +34,7 @@
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import { startPublishing } from '../../utils/liveWebRTC'
|
||||
import { startPublishing, LIVE_QUALITY_OPTIONS } from '../../utils/liveWebRTC'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const token = computed(() => authStore.getToken() || '')
|
||||
@@ -39,6 +50,7 @@ function start() {
|
||||
status.value = '正在连接…'
|
||||
const { stop } = startPublishing({
|
||||
token: token.value,
|
||||
quality: quality.value,
|
||||
onStatus: (s) => {
|
||||
status.value = s
|
||||
},
|
||||
@@ -96,6 +108,16 @@ onBeforeRouteLeave(() => {
|
||||
margin-bottom: 12px;
|
||||
min-height: 1.5em;
|
||||
}
|
||||
.quality-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.quality-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
.actions {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user