问题1 开播后 观众端第一次进直播间 直播间没有画面 需要 主播重新进直播页面 观众端才有画面问题2 上面的流程走完 观众重新进直播间 直播间看不到画面问题3 不能多观众收看直播啊

张开发
2026/4/7 3:23:49 15 分钟阅读

分享文章

问题1 开播后 观众端第一次进直播间 直播间没有画面 需要 主播重新进直播页面 观众端才有画面问题2 上面的流程走完 观众重新进直播间 直播间看不到画面问题3 不能多观众收看直播啊
需要docker srs webrtc websockdocker cmd 中 启动 srsset CANDIDATElongwen.natapp1.cc docker run --rm -it -p 1935:1935 -p 1985:1985 -p 8000:8000/udp -p 8000:8000/tcp --env CANDIDATE%CANDIDATE% --env SRS_RTC_TCP_ENABLEDon --env SRS_RTC_TCP_PORT8000 registry.cn-hangzhou.aliyuncs.com/ossrs/srs:5template div classlive-room-page div classvideo-box video refvideoRef autoplay playsinline muted crossoriginanonymous stylewidth:100%;height:100%;object-fit:cover;background:#000 /video button clickback classback-btn返回/button button v-ifisHost clickstopLive classstop-btn关闭直播/button /div div classdanmaku-box refdanmakuBoxRef div v-foritem in danmakus :keyitem.id{{ item.nickname || 用户 }}{{ item.content }}/div /div div classsend-box input v-modelmsg placeholder输入弹幕 keyup.entersendDan / button clicksendDan发送/button /div /div /template script setup import { ref, onMounted, onBeforeUnmount, nextTick } from vue import { useRouter, useRoute } from vue-router import liveApi from /api/live const router useRouter() const route useRoute() const roomId route.params.id const videoRef ref(null) const isHost ref(false) const danmakus ref([]) const msg ref() let ws null let localStream null let peer null let hasSetStream false const domain longwen.natapp1.cc const streamUrl webrtc://${domain}/live/${roomId} // // 【强制 TCP】ICE 配置只走 TCP禁用 UDP // const iceConfig { iceServers: [ { urls: stun:stun.cloudflare.com:3478 } ], iceTransportPolicy: all, bundlePolicy: max-bundle, rtcpMuxPolicy: require } // // 观众拉流强制 TCP可播放 // const startPlay async () { try { console.log(【观众】启动拉流强制 TCP 模式) hasSetStream false peer new RTCPeerConnection(iceConfig) // 流绑定 peer.ontrack (e) { if (hasSetStream) return hasSetStream true const video videoRef.value console.log( 已收到视频流开始播放) video.srcObject e.streams[0] video.load() const play () { video.muted true video.play().then(() { console.log(✅ 播放成功) }).catch(err { console.log(重试播放...) setTimeout(play, 300) }) } setTimeout(play, 500) } // ICE 状态 peer.oniceconnectionstatechange () { console.log(ICE 状态, peer.iceConnectionState) } // 关键强制支持 TCP 传输 const offer await peer.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true, iceRestart: false }) await peer.setLocalDescription(offer) const resp await fetch(/rtc/v1/play/, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ api: /rtc/v1/play/, streamurl: streamUrl, sdp: offer.sdp }) }) const ans await resp.json() await peer.setRemoteDescription({ type: answer, sdp: ans.sdp }) console.log(✅ 拉流成功等待画面...) } catch (e) { console.error(拉流失败, e) } } // // 以下不变 // const joinRoom async () { try { const res await liveApi.joinLiveRoom(roomId) if (res.code ! 200) return isHost.value res.data?.isHost || false connectWs() isHost.value ? startPublish() : startPlay() getHistoryDan() } catch (e) { console.error(e) } } const startPublish async () { try { localStream await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) videoRef.value.srcObject localStream peer new RTCPeerConnection(iceConfig) localStream.getTracks().forEach(t peer.addTrack(t, localStream)) const offer await peer.createOffer({ offerToReceiveAudio: false, offerToReceiveVideo: false }) await peer.setLocalDescription(offer) const resp await fetch(/rtc/v1/publish/, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ api: /rtc/v1/publish/, streamurl: streamUrl, sdp: offer.sdp }) }) const ans await resp.json() await peer.setRemoteDescription({ type: answer, sdp: ans.sdp }) } catch (e) { console.error(e) } } const connectWs () { const p location.protocol https: ? wss: : ws: const uid JSON.parse(localStorage.getItem(userInfo)||{}).id||0 ws new WebSocket(${p}//${location.host}/live/ws?room_id${roomId}user_id${uid}) ws.onopen () console.log(WS 连接成功) ws.onmessage (e) { try { const d JSON.parse(e.data) if (d.type danmaku) { danmakus.value.push(d.data) nextTick(() danmakuBoxRef.value.scrollTop danmakuBoxRef.value.scrollHeight) } } catch {} } } const getHistoryDan async () { const r await liveApi.getLiveDanmakus(roomId) if (r.code 200) danmakus.value r.data||[] } const sendDan async () { if (!msg.value) return await liveApi.sendDanmaku(roomId, msg.value) msg.value } const stopLive async () { await liveApi.stopLiveRoom(roomId); back() } const back () { localStream?.getTracks().forEach(tt.stop()) peer?.close() ws?.close() router.back() } onMounted(() joinRoom()) onBeforeUnmount(() back()) /script style scoped .live-room-page { height: 100vh; background: #000; color: white; display: flex; flex-direction: column; } .video-box { position: relative; height: 50vh; background: #111; } .back-btn { position: absolute; top: 20px; left: 20px; padding: 6px 12px; background: rgba(0,0,0,0.5); color: white; border: none; border-radius: 6px; } .stop-btn { position: absolute; top: 20px; right: 20px; padding: 6px 12px; background: red; color: white; border: none; border-radius: 6px; } .danmaku-box { flex: 1; padding: 10px; overflow-y: auto; } .send-box { display: flex; padding: 10px; gap: 10px; } .send-box input { flex: 1; padding: 8px; border-radius: 20px; border: none; } .send-box button { padding: 8px 16px; background: #1677ff; color: white; border: none; border-radius: 20px; } /style

更多文章