基于licenseID的WebRTC直播系统:客户端无法观看直播问题排查
问题:观众无法观看主播直播内容
我正在搭建一个可独立部署的直播服务器,通过licenseID关联主播和观众,支持多场景连接:比如主播A用licenseID 1开播,主播B用licenseID 2开播,观众可以通过对应licenseID切换观看不同主播,也能退出观看。但现在遇到的问题是,观众客户端无法看到主播的直播内容。
服务器代码
const express = require("express"); const http = require("http"); const socketIo = require("socket.io"); const cors = require("cors"); const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: "*", methods: ["GET", "POST"] } }); const port = 4000; // 用Map存储活跃主播的licenseID与socketID对应关系 const broadcasters = new Map(); app.use(cors()); // Socket连接处理 io.on("connection", socket => { console.log(`Socket已连接: ${socket.id}`); // 处理主播注册 socket.on("broadcaster", (licenseID) => { broadcasters.set(licenseID, socket.id); console.log(`主播已注册: LicenseID ${licenseID}`); }); // 处理观众观看请求 socket.on("watcher", (licenseID) => { const broadcasterSocketId = broadcasters.get(licenseID); if (broadcasterSocketId) { socket.to(broadcasterSocketId).emit("watcher", socket.id); console.log(`观众已连接到主播 ${licenseID}`); } else { socket.emit("error", "当前licenseID没有可用主播"); } }); // WebRTC信令转发 socket.on("offer", (to, message) => { socket.to(to).emit("offer", socket.id, message); }); socket.on("answer", (to, message) => { socket.to(to).emit("answer", socket.id, message); }); socket.on("candidate", (to, message) => { socket.to(to).emit("candidate", socket.id, message); }); // 断开连接处理 socket.on("disconnect", () => { console.log(`Socket已断开: ${socket.id}`); // 清理主播Map broadcasters.forEach((value, key) => { if (value === socket.id) { broadcasters.delete(key); } }); }); }); // 静态文件服务 app.use(express.static(__dirname + "/public")); // 启动服务器 server.listen(port, () => { console.log(`服务器运行在端口 ${port}`); });
原主播端代码(存在问题)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Broadcaster</title> <script src="/socket.io/socket.io.js"></script> <script> const socket = io.connect('http://localhost:4000'); let peerConnection; const licenseID = 1; const config = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }; socket.emit('broadcaster', licenseID); // Start video streaming navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(stream => { const localVideo = document.getElementById('localVideo'); localVideo.srcObject = stream; peerConnection = new RTCPeerConnection(config); stream.getTracks().forEach(track => peerConnection.addTrack(track, stream)); peerConnection.onicecandidate = (event) => { if (event.candidate) { socket.emit('candidate', licenseID, { candidate: event.candidate, to: 'watcher_id' }); } }; peerConnection.createOffer() .then(offer => peerConnection.setLocalDescription(offer)) .then(() => { socket.emit('offer', 'watcher_id', { description: peerConnection.localDescription }); }) .catch(error => { console.error('Error creating offer:', error); }); socket.on('answer', (from, message) => { peerConnection.setRemoteDescription(new RTCSessionDescription(message.description)) .catch(error => { console.error('Error setting remote description:', error); }); }); socket.on('candidate', (from, message) => { peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate)) .catch(error => { console.error('Error adding ICE candidate:', error); }); }); }) .catch(error => { console.error('Error accessing media devices: ', error); }); </script> </head> <body> <h1>Broadcaster</h1> <video id="localVideo" autoplay muted></video> </body> </html>
原观众端代码(存在问题)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Watcher</title> <script src="/socket.io/socket.io.js"></script> <script> const socket = io.connect('http://localhost:4000'); let peerConnection; const licenseID = 1; const config = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }; socket.emit('watcher', licenseID); socket.on('offer', async (from, message) => { console.log("Received offer from broadcaster:", from, message); peerConnection = new RTCPeerConnection(config); peerConnection.ontrack = (event) => { const remoteVideo = document.getElementById('remoteVideo'); remoteVideo.srcObject = event.streams[0]; }; peerConnection.onicecandidate = (event) => { if (event.candidate) { socket.emit('candidate', 'broadcaster_id', { candidate: event.candidate, to: from }); } }; try { await peerConnection.setRemoteDescription(new RTCSessionDescription(message.description)); const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); socket.emit('answer', from, { description: peerConnection.localDescription }); } catch (e) { console.error("Error establishing connection:", e); } }); socket.on('candidate', (from, message) => { console.log("Received ICE candidate from broadcaster:", from, message); if (peerConnection) { peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate)); } }); </script> </head> <body> <h1>Watcher</h1> <video id="remoteVideo" autoplay controls></video> </body> </html>
问题根源与修复方案
1. 主播端核心问题:未监听观众连接事件,提前创建Offer
原主播端在获取媒体流后直接创建Offer并发送,但此时还没有观众请求连接,而且目标ID写死为'watcher_id',服务器无法正确转发。正确逻辑应该是:当服务器通知有观众连接(watcher事件)时,再创建PeerConnection并发送Offer给该观众。
2. ICE候选发送目标错误
- 主播端发送ICE候选时,错误地将licenseID作为目标参数,服务器需要的是观众的socket ID
- 观众端发送ICE候选时,写死目标为
'broadcaster_id',应该用收到Offer时的主播socket ID(from参数)
修复后的主播端代码
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>主播端</title> <script src="/socket.io/socket.io.js"></script> <script> const socket = io.connect('http://localhost:4000'); let localStream; const licenseID = 1; // 存储每个观众对应的PeerConnection const peerConnections = new Map(); const config = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }; socket.emit('broadcaster', licenseID); // 获取本地媒体流 navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(stream => { localStream = stream; const localVideo = document.getElementById('localVideo'); localVideo.srcObject = stream; }) .catch(error => { console.error('获取媒体设备失败: ', error); }); // 监听服务器发来的观众连接请求 socket.on('watcher', (watcherSocketId) => { if (!localStream) return; // 为该观众创建新的PeerConnection const peerConnection = new RTCPeerConnection(config); peerConnections.set(watcherSocketId, peerConnection); // 添加本地媒体轨道 localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream)); // 发送ICE候选给观众 peerConnection.onicecandidate = (event) => { if (event.candidate) { socket.emit('candidate', watcherSocketId, { candidate: event.candidate }); } }; // 创建并发送Offer给观众 peerConnection.createOffer() .then(offer => peerConnection.setLocalDescription(offer)) .then(() => { socket.emit('offer', watcherSocketId, { description: peerConnection.localDescription }); }) .catch(error => { console.error('创建Offer失败:', error); }); // 处理观众发来的Answer socket.on('answer', (from, message) => { if (from === watcherSocketId) { peerConnection.setRemoteDescription(new RTCSessionDescription(message.description)) .catch(error => { console.error('设置远端描述失败:', error); }); } }); // 处理观众发来的ICE候选 socket.on('candidate', (from, message) => { if (from === watcherSocketId && peerConnection) { peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate)) .catch(error => { console.error('添加ICE候选失败:', error); }); } }); }); </script> </head> <body> <h1>主播端</h1> <video id="localVideo" autoplay muted></video> </body> </html>
修复后的观众端代码
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>观众端</title> <script src="/socket.io/socket.io.js"></script> <script> const socket = io.connect('http://localhost:4000'); let peerConnection; const licenseID = 1; const config = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }; socket.emit('watcher', licenseID); // 处理主播发来的Offer socket.on('offer', async (broadcasterSocketId, message) => { console.log("收到主播Offer:", broadcasterSocketId, message); peerConnection = new RTCPeerConnection(config); // 接收主播的媒体流 peerConnection.ontrack = (event) => { const remoteVideo = document.getElementById('remoteVideo'); remoteVideo.srcObject = event.streams[0]; }; // 发送ICE候选给主播 peerConnection.onicecandidate = (event) => { if (event.candidate) { socket.emit('candidate', broadcasterSocketId, { candidate: event.candidate }); } }; try { await peerConnection.setRemoteDescription(new RTCSessionDescription(message.description)); const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); // 发送Answer给主播 socket.emit('answer', broadcasterSocketId, { description: peerConnection.localDescription }); } catch (e) { console.error("建立连接失败:", e); } }); // 处理主播发来的ICE候选 socket.on('candidate', (from, message) => { console.log("收到主播ICE候选:", from, message); if (peerConnection) { peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate)); } }); </script> </head> <body> <h1>观众端</h1> <video id="remoteVideo" autoplay controls></video> </body> </html>
内容的提问来源于stack exchange,提问作者Good
相关产品推荐
相关产品推荐

