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

WebRTC连接后仅单端摄像头可用问题求助

WebRTC单向视频流问题排查与修复

我开发了一个通过WebSocket传输SDP描述和ICE候选、自动建立用户连接的程序,但连接完成后仅一侧摄像头正常工作,另一端的媒体流摄像头处于未激活状态。已经核对过ICE候选信息、验证了SDP描述的正确性,也确认信令通信流程正常,但无法定位问题根源。相关代码如下:

App.jsx

import { useEffect } from 'react'
import { getMedia, connectionData} from './connection'
import './App.css'

function App() {
  useEffect(() => {
    connectionData.localStream = document.getElementById("localVideoStream");
    connectionData.remoteStream = document.getElementById("remoteStream");
    getMedia();
  }, [])

  return (
    <div>
      <video id ="localVideoStream" autoPlay></video>
      <video  id = "remoteStream" autoPlay style={{
        height: `${250}px`,
        width: `${250}px`,
        border: `solid ${1}px black`
      }}></video>
    </div>
  )
}

export default App

connection.js

const webSocket = new WebSocket("ws://localhost:3000/");

let connectionData = {
    pcConnection: new RTCPeerConnection(null),
    remoteStream: null,
    localStream: null,
    connectionCount: 0,
}

let candidateManager ={
    sendet:false,
    candidateAdded:false
};

let candidateSendet = false;
//für später
let connectionDataList = [];


//web Socket funktionen
webSocket.onopen = function (e) {
    createOffer();
}

webSocket.onmessage = function (e) {
    let sdpObject = JSON.parse(e.data);
    if (sdpObject.type == "offer") {
        console.log("offer");
        setRemoteDescription(sdpObject);
        createAnswer();
    }
    if (sdpObject.type == "answer") {
        console.log("answer");
        setRemoteDescription(sdpObject);
    }

    if (sdpObject.type == "candidate") {
        console.log("candidate");
        console.log(e);
        //console.log("streams: ", connectionData.remoteStream.srcObject);
        setCandidate(sdpObject.candidate);
    }
}

//stream functions
async function getMedia() {
    const constraints = {
        audio: false,
        video: true,
    }

    await navigator.mediaDevices.getUserMedia(constraints)
        .then(stream => {
            connectionData.localStream.srcObject = stream;
            stream.getTracks().forEach(track => {
                //console.log("user Media", stream);
                connectionData.pcConnection.addTrack(track, stream);
            })
        })
}

function createOffer() {
    connectionData.pcConnection.createOffer({
        offerToReceiveAudio: 1,
        offerToReceiveVideo: 1,
    }).then(async sdp => {
        webSocket.send(JSON.stringify(sdp));
        await connectionData.pcConnection.setLocalDescription(sdp);
        candidateManager.sendet=false;
    });
}

function createAnswer() {
    connectionData.pcConnection.createAnswer({
        offerToReceiveAudio: 1,
        offerToReceiveVideo: 1,
    }).then(async sdp => {
        webSocket.send(JSON.stringify(sdp));
        await connectionData.pcConnection.setLocalDescription(sdp);
    });
}

async function setRemoteDescription(sdp) {
    await connectionData.pcConnection.setRemoteDescription(new RTCSessionDescription(sdp))
}

function setCandidate(candidate) {
    if (connectionData.pcConnection.remoteDescription != null) {
        connectionData.pcConnection.addIceCandidate(new RTCIceCandidate(candidate)).catch((e)=>{
            console.log(`failure MESSAGE:${e.name}`)
        });
        console.log(connectionData.remoteStream.srcObject);
    }
}

connectionData.pcConnection.onicecandidate = function (e) {
    if (e.candidate != null) {
        const candidate = {
            type: "candidate",
            candidate: e.candidate
        }
        if (candidate.candidate.protocol == "udp" && candidateManager.sendet==false) {
            webSocket.send(JSON.stringify(candidate));
            console.log("sendet Candidate",candidate);
            candidateManager.sendet = true;
        }
    }
}

connectionData.pcConnection.ontrack = (e) => {
    console.log("onTrack", e.streams[0]);
    connectionData.remoteStream.srcObject = e.streams[0];
    //console.log("remoteStream: ", e.streams);
}

export {
    webSocket,
    connectionData,
    getMedia,
    connectionDataList,
    candidateSendet
}

问题根源分析

  1. ICE候选发送限制过严:当前代码仅发送第一个UDP候选,且发送后就停止发送后续候选。WebRTC需要交换多个ICE候选来适配不同网络环境,单个候选可能无法建立双向媒体流连接,导致一侧无法接收视频。
  2. 异步逻辑不规范:getMedia混用await和.then,且无错误捕获,若媒体流获取失败会静默失败,导致后续addTrack无有效流。
  3. 信令顺序问题:创建Offer/Answer时先发送SDP再设置本地描述,可能导致本地状态未同步就发送信令,引发状态不一致。
  4. 错误处理不完整:setRemoteDescription等关键操作未完整捕获错误,不利于排查潜在问题。

修复方案

1. 取消ICE候选发送限制

发送所有收集到的UDP候选,直到候选收集完成:

connectionData.pcConnection.onicecandidate = function (e) {
    if (e.candidate != null) {
        const candidate = {
            type: "candidate",
            candidate: e.candidate
        };
        if (candidate.candidate.protocol === "udp") {
            webSocket.send(JSON.stringify(candidate));
            console.log("sent Candidate", candidate);
        }
    } else {
        // 可选:发送候选收集完成标识
        webSocket.send(JSON.stringify({ type: "candidate-done" }));
    }
};

2. 规范异步逻辑并添加错误处理

修改getMedia为纯async/await写法,添加错误捕获:

async function getMedia() {
    const constraints = {
        audio: false,
        video: true,
    };
    try {
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        connectionData.localStream.srcObject = stream;
        stream.getTracks().forEach(track => {
            connectionData.pcConnection.addTrack(track, stream);
        });
    } catch (err) {
        console.error("获取媒体流失败:", err);
    }
}

3. 调整信令顺序

先设置本地描述再发送SDP,确保本地状态同步:

async function createOffer() {
    try {
        const sdp = await connectionData.pcConnection.createOffer({
            offerToReceiveAudio: 1,
            offerToReceiveVideo: 1,
        });
        await connectionData.pcConnection.setLocalDescription(sdp);
        webSocket.send(JSON.stringify(sdp));
    } catch (err) {
        console.error("创建Offer失败:", err);
    }
}

async function createAnswer() {
    try {
        const sdp = await connectionData.pcConnection.createAnswer({
            offerToReceiveAudio: 1,
            offerToReceiveVideo: 1,
        });
        await connectionData.pcConnection.setLocalDescription(sdp);
        webSocket.send(JSON.stringify(sdp));
    } catch (err) {
        console.error("创建Answer失败:", err);
    }
}

4. 完善关键操作的错误处理

给setRemoteDescription添加错误捕获:

async function setRemoteDescription(sdp) {
    try {
        await connectionData.pcConnection.setRemoteDescription(new RTCSessionDescription(sdp));
    } catch (err) {
        console.error("设置远端描述失败:", err);
    }
}

5. 优化ontrack逻辑

避免直接覆盖srcObject,已有流时添加轨道:

connectionData.pcConnection.ontrack = (e) => {
    console.log("onTrack", e.streams[0]);
    if (!connectionData.remoteStream.srcObject) {
        connectionData.remoteStream.srcObject = e.streams[0];
    } else {
        e.streams[0].getTracks().forEach(track => {
            connectionData.remoteStream.srcObject.addTrack(track);
        });
    }
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 11:17:17