直播:转发音频、观众 recv audio、全屏与开声音按钮、后台采集麦克风
Made-with: Cursor
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -58,6 +58,7 @@ const showSurface = ref(false)
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
session = startViewing(videoRef.value, {
|
||||
muted: true,
|
||||
onStatus: (s) => {
|
||||
hint.value = s
|
||||
},
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -10,13 +10,18 @@
|
||||
<section class="live-block" aria-label="本站直播">
|
||||
<h2 class="live-block-title">本站直播(WebRTC)</h2>
|
||||
<p class="live-watch-status">{{ watchStatus }}</p>
|
||||
<video
|
||||
ref="watchVideoRef"
|
||||
class="live-room-video live-room-video--watch"
|
||||
playsinline
|
||||
autoplay
|
||||
muted
|
||||
></video>
|
||||
<div class="live-video-wrap">
|
||||
<video
|
||||
ref="watchVideoRef"
|
||||
class="live-room-video live-room-video--watch"
|
||||
playsinline
|
||||
autoplay
|
||||
></video>
|
||||
<div class="live-video-toolbar" role="toolbar" aria-label="播放控制">
|
||||
<button type="button" class="live-video-toolbtn" @click="unmuteAndPlay">开声音</button>
|
||||
<button type="button" class="live-video-toolbtn" @click="toggleVideoFullscreen">全屏</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="live-block live-block--divider" aria-label="外链直播间">
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user