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);