From 6b3210f71433c526d91ed4a93b623cee2137995a Mon Sep 17 00:00:00 2001 From: whm <973418690@qq.com> Date: Thu, 26 Mar 2026 09:55:27 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=B4=E6=92=AD=EF=BC=9A=E8=BD=AC=E5=8F=91?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E3=80=81=E8=A7=82=E4=BC=97=20recv=20audio?= =?UTF-8?q?=E3=80=81=E5=85=A8=E5=B1=8F=E4=B8=8E=E5=BC=80=E5=A3=B0=E9=9F=B3?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E3=80=81=E5=90=8E=E5=8F=B0=E9=87=87=E9=9B=86?= =?UTF-8?q?=E9=BA=A6=E5=85=8B=E9=A3=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- admin/src/utils/liveWebRTC.js | 4 +- server/pkg/weblive/hub.go | 3 +- web/src/components/LiveFloatingPlayer.vue | 1 + web/src/utils/liveWebRTC.js | 23 ++++++-- web/src/views/LiveRoom.vue | 71 ++++++++++++++++++++--- 5 files changed, 86 insertions(+), 16 deletions(-) diff --git a/admin/src/utils/liveWebRTC.js b/admin/src/utils/liveWebRTC.js index 3686f6a..542f2d1 100644 --- a/admin/src/utils/liveWebRTC.js +++ b/admin/src/utils/liveWebRTC.js @@ -21,7 +21,7 @@ function humanizeGetUserMediaError(err) { const name = err && err.name const raw = ((err && err.message) || '').toLowerCase() if (name === 'NotAllowedError' || name === 'PermissionDeniedError') { - return '已拒绝摄像头权限:在浏览器地址栏左侧允许摄像头,并确认本页为 HTTPS。' + return '已拒绝摄像头或麦克风权限:在浏览器地址栏左侧允许摄像头与麦克风,并确认本页为 HTTPS。' } if (name === 'NotFoundError' || name === 'DevicesNotFoundError') { return '未检测到摄像头,请检查是否已接入设备或被系统禁用。' @@ -75,7 +75,7 @@ export function startPublishing(opts = {}) { onStatus('信令已连接,正在采集摄像头…') try { // 后台多为 PC:不要用 facingMode:'user',部分机器会直接导致 “Could not start video source” - stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }) + stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) onLocalStream(stream) stream.getTracks().forEach((t) => pc.addTrack(t, stream)) const offer = await pc.createOffer() diff --git a/server/pkg/weblive/hub.go b/server/pkg/weblive/hub.go index 747183e..cdc3c1c 100644 --- a/server/pkg/weblive/hub.go +++ b/server/pkg/weblive/hub.go @@ -146,8 +146,7 @@ func (h *Hub) removeViewer(id string) { } func (h *Hub) onPublisherTrack(track *webrtc.TrackRemote) { - // 仅转发视频轨,降低协商复杂度 - if track.Kind() != webrtc.RTPCodecTypeVideo { + if track.Kind() != webrtc.RTPCodecTypeVideo && track.Kind() != webrtc.RTPCodecTypeAudio { return } tf := newTrackForwarder(track) diff --git a/web/src/components/LiveFloatingPlayer.vue b/web/src/components/LiveFloatingPlayer.vue index 787350d..d95ed3e 100644 --- a/web/src/components/LiveFloatingPlayer.vue +++ b/web/src/components/LiveFloatingPlayer.vue @@ -58,6 +58,7 @@ const showSurface = ref(false) onMounted(async () => { await nextTick() session = startViewing(videoRef.value, { + muted: true, onStatus: (s) => { hint.value = s }, diff --git a/web/src/utils/liveWebRTC.js b/web/src/utils/liveWebRTC.js index f774ed3..77bfe36 100644 --- a/web/src/utils/liveWebRTC.js +++ b/web/src/utils/liveWebRTC.js @@ -35,13 +35,27 @@ function newPeer(onTrack) { /** * 观众端:轮询 live 后拉流;结束后自动恢复轮询以便下一场 * @param {HTMLVideoElement | null} videoEl - * @param {{ onStatus?: (s: string) => void, onLive?: () => void, onEnded?: () => void, pollMs?: number }} [opts] + * @param {{ onStatus?: (s: string) => void, onLive?: () => void, onEnded?: () => void, pollMs?: number, muted?: boolean }} [opts] */ export function startViewing(videoEl, opts = {}) { - const { onStatus = () => {}, onLive = () => {}, onEnded = () => {}, pollMs = 1200 } = opts + const { + onStatus = () => {}, + onLive = () => {}, + onEnded = () => {}, + pollMs = 1200, + muted = true + } = opts + + function attachStreamToVideo(stream) { + if (!videoEl || !stream) return + videoEl.srcObject = stream + videoEl.muted = !!muted + videoEl.play().catch(() => {}) + } + let pc = newPeer((e) => { if (videoEl && e.streams[0]) { - videoEl.srcObject = e.streams[0] + attachStreamToVideo(e.streams[0]) } }) let ws = null @@ -98,7 +112,7 @@ export function startViewing(videoEl, opts = {}) { } catch (_) {} pc = newPeer((e) => { if (videoEl && e.streams[0]) { - videoEl.srcObject = e.streams[0] + attachStreamToVideo(e.streams[0]) } }) } @@ -129,6 +143,7 @@ export function startViewing(videoEl, opts = {}) { else icePending.push(msg) } pc.addTransceiver('video', { direction: 'recvonly' }) + pc.addTransceiver('audio', { direction: 'recvonly' }) const offer = await pc.createOffer() await pc.setLocalDescription(offer) ws = new WebSocket(liveWsURLView()) diff --git a/web/src/views/LiveRoom.vue b/web/src/views/LiveRoom.vue index e6c6a71..59e1af8 100644 --- a/web/src/views/LiveRoom.vue +++ b/web/src/views/LiveRoom.vue @@ -10,13 +10,18 @@

本站直播(WebRTC)

{{ watchStatus }}

- +
+ + +
@@ -80,10 +85,34 @@ function goLiveRoom() { window.location.href = target } +function unmuteAndPlay() { + const v = watchVideoRef.value + if (!v) return + v.muted = false + v.play().catch(() => {}) +} + +function toggleVideoFullscreen() { + const v = watchVideoRef.value + if (!v) return + const doc = document + if (doc.fullscreenElement || doc.webkitFullscreenElement) { + doc.exitFullscreen?.() || doc.webkitExitFullscreen?.() + return + } + if (typeof v.webkitEnterFullscreen === 'function') { + v.webkitEnterFullscreen() + return + } + const el = v + el.requestFullscreen?.() || el.webkitRequestFullscreen?.() +} + onMounted(async () => { loadHomepage() await nextTick() viewSession = startViewing(watchVideoRef.value, { + muted: false, onStatus: (s) => { watchStatus.value = s } @@ -153,6 +182,33 @@ onUnmounted(() => { margin: 0 0 14px; min-height: 1.4em; } +.live-video-wrap { + position: relative; + max-width: 480px; + margin: 0 auto; +} +.live-video-toolbar { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: center; + margin-top: 12px; +} +.live-video-toolbtn { + padding: 8px 18px; + border-radius: 10px; + border: 1px solid rgba(0, 212, 255, 0.45); + background: rgba(0, 212, 255, 0.12); + color: #00d4ff; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s, border-color 0.2s; +} +.live-video-toolbtn:hover { + background: rgba(0, 212, 255, 0.22); + border-color: rgba(0, 212, 255, 0.75); +} .live-room-actions { margin-top: 8px; } @@ -186,7 +242,6 @@ onUnmounted(() => { .live-room-video { display: block; width: 100%; - max-width: 480px; margin: 0 auto; border-radius: 14px; border: 1px solid rgba(0, 212, 255, 0.25);