You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Laravel9+React的流媒体应用观众端音视频连接失败问题

问题:观众端无法获取主播音视频,Simple Peer连接失败

使用Laravel 9、React Js、Laravel Echo、Pusher和Simple Peer搭建流媒体应用,当前在观众端无法获取主播的音视频,出现以下错误:

Uncaught Error: Connection failed. at p._onConnectionStateChange (simplepeer.min.js:6:91542) at _pc.onconnectionstatechange (simplepeer.min.js:6:81452)

观众端React代码

import React,{useEffect, useRef, useState} from 'react'
import ReactDOM from 'react-dom';
import axios from 'axios'
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
import Peer from  'simple-peer/simplepeer.min.js';
export default function Viewer() {
    const [broadcasterId, setBroadcasterId] = useState(null)
    const vidRef = useRef();
    window.Pusher = Pusher;
    window.Echo = new Echo({
        broadcaster: 'pusher',
        key: import.meta.env.VITE_PUSHER_APP_KEY,
        wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
        wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
        wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
        forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
        enabledTransports: ['ws', 'wss'],
        cluster:import.meta.env.VITE_PUSHER_APP_CLUSTER
    });

    const handleViewer =()=>{
        initializeStreamingChannel()
        initializeSignalOfferChannel()
    }

    function initializeStreamingChannel(){
       let streamingPresenceChannel = window.Echo.join(`streaming-channel.${streamId}`)

    }


    function initializeSignalOfferChannel(){
       window.Echo.private(`stream-signal-channel.${myid}`).listen('StreamOffer',
        ({data}) => {
            setBroadcasterId(data.broadcaster);
            createViewerPeer(data.offer, data.broadcaster);

          }
        )

    }



    function createViewerPeer(offer, broadcaster){
        console.log(offer)
           let peer = new Peer({
            initiator: false,
            trickle: false,
            config: {
              iceServers: [
                {
                  urls:['stun:stun1.1.google.com:19302', 'stun:stun2.1.google.com:19302']
                },
                // {
                //   urls: this.turn_url,
                //   username: this.turn_username,
                //   credential: this.turn_credential,
                // },
              ],
            },

           });
           peer.addTransceiver("video", { direction: "recvonly" });
           peer.addTransceiver("audio", { direction: "recvonly" });
           handlePeerEvents(peer, offer, broadcaster)
    }

    function handlePeerEvents(peer, offer, broadcaster){

        peer.on("signal", (data) => {
            let formData = new FormData();
            formData.append("broadcaster",  broadcaster)
            formData.append("answer", data)
            axios.post("http://127.0.0.1:8000/stream-answer", formData)
              .then((res) => {
                console.log(res);
              })
              .catch((err) => {
                console.log(err);
              });
          });


          peer.on("stream", (stream) => {
            console.log(stream)
            // display remote stream
            vidRef.current.srcObject = stream
       
          });

          peer.on("track", (track, stream) => {
            console.log("onTrack");
          });

          peer.on("connect", () => {
            console.log("Viewer Peer connected");
          });


          peer.on("close", () => {
            console.log("Viewer Peer closed");
            peer.destroy();
          });


          const updatedOffer = {
            ...offer,
            sdp: `${offer.sdp}\n`,
          };

          peer.signal(updatedOffer);
    }

    return (
        <div>

            Viewer
            <video  style={{width:'500px', height:'420px' }} autoPlay={true} ref={vidRef} >
            </video>
           <button onClick={handleViewer}>join stream</button>
        </div>
    )
}

if (document.getElementById('view')) {
    ReactDOM.render(<Viewer/>, document.getElementById('view'));
}

排查与修复方案

  • 移除SDP手动修改:代码中手动给SDP添加换行符可能破坏格式,直接使用原始offer:
    // 删掉这部分
    // const updatedOffer = {
    //   ...offer,
    //   sdp: `${offer.sdp}\n`,
    // };
    peer.signal(offer);
    
  • 添加错误监听:给Peer实例添加error事件,定位具体失败原因:
    peer.on("error", (err) => {
      console.error("Peer连接错误详情:", err);
    });
    
  • 管理Peer实例生命周期:将Peer实例存入useRef,避免局部变量被回收:
    const peerRef = useRef(null);
    
    function createViewerPeer(offer, broadcaster){
      peerRef.current = new Peer({...});
      // 后续操作使用peerRef.current
    }
    
  • 配置TURN服务器:如果测试环境不在同一局域网,仅STUN无法穿透NAT,需启用TURN服务,填写有效的TURN地址、用户名和凭证:
    iceServers: [
      { urls:['stun:stun1.1.google.com:19302', 'stun:stun2.1.google.com:19302'] },
      {
        urls: 'your-turn-server-url',
        username: 'turn-username',
        credential: 'turn-password',
      },
    ]
    
  • 检查跨域与请求有效性:确认Laravel后端已配置CORS允许前端域名访问,且/stream-answer接口能正确接收并转发answer给主播端。
  • 优化视频标签属性:添加playsInline属性,适配移动端和部分浏览器的播放规则:
    <video style={{width:'500px', height:'420px' }} autoPlay playsInline ref={vidRef} />
    
  • 验证Pusher事件数据:确保后端触发的StreamOffer事件中,offer对象包含完整的type和sdp字段,无格式错误。

内容的提问来源于stack exchange,提问作者stephen jason

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 10:20:41