Elixir-MembraneWebRTC/React中MediaStream客户端显示异常求助
问题描述
我正在扩展Membrane Videoroom教程中的客户端,但无法让远程流在客户端显示——要么stream为null,要么stream非空但没有任何内容显示。
核心代码:useGala Hook
const useGala = roomId => { const { socket } = useContext(SocketContext); const permChannel = socket.channel(`room:${roomId}`); const [gala, setGala] = useState({ channel: permChannel, localStream: null, peers: [], streams: [], webrtcEngine: null, initialized: false, }); const joinWebrtc = async (engine) => { engine.join({ displayName: "Will" }); } const createLocalStream = async (engine) => { const localStream = await navigator.mediaDevices.getUserMedia(MEDIA_CONSTRAINTS); localStream.getTracks().forEach((track) => { engine.addTrack(track, localStream) }); setGala(prev => ({ ...prev, localStream: localStream })); } const makeEngine = async () => { // Create the WebRTC engine const engine = new MembraneWebRTC({ callbacks: { onSendMediaEvent: (mediaEvent) => { console.log('gala:onSendMediaEvent: fired'); gala.channel.push("mediaEvent", { data: mediaEvent }); }, onConnectionError: () => { console.log('on connection error triggered') }, onJoinSuccess: (peerId, peersInRoom) => { console.log("gala:onJoinSuccess: fired"); setGala(prev => ({ ...prev, peers: peersInRoom })); createLocalStream(engine); }, onJoinError: (metadata) => { throw `Peer denied.`; }, onTrackReady: ({ stream, peer, metadata }) => { console.log("gala:onTrackReady: fired"); const inboundStream = { peerId: `${peer.id}`, stream: stream }; // This fires, but the `stream` object is either // 1. null if on the already joined client // 2. non-null if on the newly joined client setGala(prev => ({ ...prev, streams: [...prev.streams, inboundStream] })); }, onTrackAdded: (ctx) => { console.log("hook:onTrackAdded: fired"); console.log(ctx); }, onTrackRemoved: (ctx) => { }, onPeerJoined: (peer) => { console.log("hook:onPeerJoined: fired"); setGala(prev => { return { ...prev, peers: [...prev.peers, peer] } }); }, onPeerLeft: (peer) => { console.log("hook:onPeerLeft: fired"); setGala(prev => { const remainingPeers = prev.peers.filter((p) => p.id !== peer.id); return { ...prev, peers: remainingPeers } }); }, onPeerUpdated: (ctx) => { }, } }); return engine; } const otherStuff = async (engine) => { if (gala.channel.state === 'joined' && !!engine) { // Setup a tracker for inbound media gala.channel.on("mediaEvent", (event) => { // For already-joined client, does not fire if new peer joins // For new peer, fires engine.receiveMediaEvent(event.data) }); setGala(prev => ({ ...prev, initialized: true, webrtcEngine: engine })) } } const doEverything = async () => { let engine; if (!gala.webrtcEngine) { engine = await makeEngine(); } // Join the server with the new stream const phoenixChannelPushResult = async (push) => { return new Promise((resolve, reject) => { push .receive("ok", (response) => resolve(response)) .receive("error", (response) => reject(response)); }); }; if (gala.channel.state !== 'joining' && gala.channel.state !== 'joined' && (!!engine || gala.webrtcEngine)) { await phoenixChannelPushResult(gala.channel.join()); if (engine) { await joinWebrtc(engine); return engine } else { return gala.webrtcEngine; } } } const all = useCallback(async () => { doEverything() .then(engine => otherStuff(engine)); }, [gala.channel]); const doStuffWithChannel = useCallback(async () => { if (gala?.streams?.length > 0) { // Setup a tracker for inbound media // Set this up to see if it would fire, but nothing gala.channel.on("mediaEvent", (event) => { gala.webrtcEngine.receiveMediaEvent(event.data) }); } }, [gala.streams, gala.channel]); useEffect(() => { if (!socket) return; all(); doStuffWithChannel(); }, [socket]); return gala; }
媒体渲染组件:MediaElement
const MediaElement = ({ peerId, gala }) => { const audioRef = useRef(); const videoRef = useRef(); useEffect(() => { if (gala?.streams?.length > 0) { audioRef.current.srcObject = gala?.streams[gala?.streams?.length - 1]?.stream; videoRef.current.srcObject = gala?.streams[gala?.streams?.length - 1]?.stream; } }, [gala?.streams, gala.channel, gala.webrtcEngine]); return ( <div className='col-span-full col-start-3 row-span-3' key={peerId}> <p className='text-white'>{peerId}</p> <audio id={`audio-${peerId}`} ref={audioRef} volume="true" autoPlay></audio> <video id={`video-${peerId}`} ref={videoRef} playsInline autoPlay></video> </div> ); } export default MediaElement;
我认为错误与webRtcEngine的状态管理方式有关,因为这是与原生JavaScript示例的主要区别。但我不知道该如何修复,请问如何让MediaStreams正常显示?
解决方案
你的问题集中在闭包陷阱、事件监听重复注册、流匹配逻辑错误三个核心点,以下是分步修复方案:
1. 修复WebRTC引擎回调的闭包问题
makeEngine中创建的回调引用的是初始gala状态,导致gala.channel可能不是最新值。直接使用初始化时的permChannel替代:
onSendMediaEvent: (mediaEvent) => { console.log('gala:onSendMediaEvent: fired'); permChannel.push("mediaEvent", { data: mediaEvent }); }
2. 确保mediaEvent监听器只注册一次
当前otherStuff和doStuffWithChannel重复注册监听,导致逻辑混乱。将监听移到doEverything中,仅在channel首次连接成功后注册:
const doEverything = async () => { let engine; if (!gala.webrtcEngine) { engine = await makeEngine(); } const phoenixChannelPushResult = async (push) => { return new Promise((resolve, reject) => { push .receive("ok", (response) => resolve(response)) .receive("error", (response) => reject(response)); }); }; if (gala.channel.state !== 'joining' && gala.channel.state !== 'joined' && (!!engine || gala.webrtcEngine)) { await phoenixChannelPushResult(gala.channel.join()); // 先移除旧监听,避免重复绑定 gala.channel.off("mediaEvent"); gala.channel.on("mediaEvent", (event) => { (engine || gala.webrtcEngine).receiveMediaEvent(event.data); }); if (engine) { // 先创建本地流再加入房间,符合Membrane流程 await createLocalStream(engine); await joinWebrtc(engine); setGala(prev => ({ ...prev, initialized: true, webrtcEngine: engine })); return engine; } else { return gala.webrtcEngine; } } }
同时删除otherStuff和doStuffWithChannel中的重复监听逻辑。
3. 修复MediaElement的流匹配逻辑
当前总是取streams最后一项,导致所有组件绑定同一个流。修改为根据peerId匹配对应流:
const MediaElement = ({ peerId, gala }) => { const audioRef = useRef(); const videoRef = useRef(); useEffect(() => { const targetStream = gala.streams.find(item => item.peerId === peerId)?.stream; if (targetStream) { audioRef.current.srcObject = targetStream; videoRef.current.srcObject = targetStream; } }, [gala.streams, peerId]); return ( <div className='col-span-full col-start-3 row-span-3' key={peerId}> <p className='text-white'>{peerId}</p> <audio id={`audio-${peerId}`} ref={audioRef} autoPlay playsInline></audio> <video id={`video-${peerId}`} ref={videoRef} playsInline autoPlay muted={peerId === localPeerId}></video> </div> ); }
注:本地流建议添加muted属性避免回声,需要在父组件传入localPeerId
4. 修复TrackReady的空流问题
onTrackReady返回空流是因为轨道未完全就绪,改用onTrackAdded处理流的创建与更新:
onTrackAdded: ({ track, stream, peer }) => { console.log("hook:onTrackAdded: fired"); setGala(prev => { // 检查是否已存在该peer的流 const existingStreamIndex = prev.streams.findIndex(s => s.peerId === peer.id); if (existingStreamIndex !== -1) { // 已有流则直接添加轨道 const updatedStreams = [...prev.streams]; updatedStreams[existingStreamIndex].stream.addTrack(track); return { ...prev, streams: updatedStreams }; } else { // 无对应流则新增 return { ...prev, streams: [...prev.streams, { peerId: peer.id, stream }] }; } }); }
同时删除onTrackReady中的流添加逻辑,避免重复。
内容的提问来源于stack exchange,提问作者nonamorando
相关产品推荐
相关产品推荐

