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

react-native-webrtc远程流显示空白/黑屏问题求助

远程视频流显示空白/黑屏问题排查

问题描述

我被这个问题卡了3天,开发一个简单的视频通话应用时,远程流显示空白。给RTCView添加objectFit="cover"属性后,会变成黑屏而非空白。日志显示远程流已被接收,但仍无法正常显示,想问这是STUN服务器的问题还是其他原因?

代码实现

import React, { useState, useEffect } from 'react';
import { View, Text, Button } from 'react-native';
import { RTCPeerConnection, RTCView, mediaDevices } from 'react-native-webrtc';
import socket from './socket';

const STUN_SERVERS = [
  { urls: 'stun:stun.l.google.com:19302' },
  // Add more STUN servers if needed
];
const VideoCall = () => {
  const [localStream, setLocalStream] = useState(null);
  const [remoteStream, setRemoteStream] = useState(null);
  const [peerConnections, setPeerConnections] = useState([]);
  const socketInstance =  socket()

  useEffect(() => {
    // Initialize local stream
    mediaDevices.getUserMedia({ audio: true, video: true })
      .then(stream => setLocalStream(stream))
      .catch(error => console.error('Error getting user media:', error));

    return () => {
      socketInstance.disconnect(); // Clean up socketInstance connection on component unmount
    };
  }, []);
  
  useEffect(() => {
    if (localStream) {
      // Create a peer connection for each user
      const pc = new RTCPeerConnection({
        iceServers: STUN_SERVERS,
      });
      setPeerConnections(prevConnections => [...prevConnections, pc]);
  
      // Add local stream to peer connection
      localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
  
      // Handle ICE candidate events
      pc.onicecandidate = event => {
        if (event.candidate) {
          // Send ICE candidate to all connected clients
          socketInstance.emit('ice-candidate', { candidate: event.candidate });
        }
      };
  
      // Handle remote stream events
      pc.ontrack = event => {
        setRemoteStream(event.streams[0]);
      };
    }
  }, [localStream]);
  
  useEffect(()=>{
    if (localStream){
      socketInstance.on('offer', data => {
        handleOffer(data);
      });
    }
  }, [localStream])

  const handleOffer = data => {
    if (!localStream) {
      console.error('Local stream is not yet available');
      return;
    }
    console.log('offerHandle Called')
    const pc = new RTCPeerConnection({
      iceServers: STUN_SERVERS,
    });
    setPeerConnections(prevConnections => [...prevConnections, pc]);
  
    // Add local stream to peer connection
    localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
  
    pc.onicecandidate = event => {
      if (event.candidate) {
        socketInstance.emit('ice-candidate', { candidate: event.candidate });
      }
    };
  
    pc.ontrack = event => {
      console.log("Received tracks:", event.streams[0].getTracks()); // Log the tracks
      // Check the kind of the track
      if (event.track.kind === 'video') {
        // If it's a video track, update remoteStream
        setRemoteStream(event.streams[0]);
      }
      // You can also handle audio tracks if needed
    };
  
    pc.setRemoteDescription(data.offer)
      .then(() => pc.createAnswer())
      .then(answer => {
        socketInstance.emit('answer', {
          answer,
          senderSocketId: data.senderUserId
        });
        return pc.setLocalDescription(answer);
      });
  };
  
  
  useEffect(() => {
    console.log('remote stream inUseeffect: ', remoteStream);
  }, [remoteStream]);
       
  const handleCall = () => {
    // Send offer to all connected clients
    peerConnections.forEach(pc => {
      pc.createOffer()
        .then(offer => {
          // Send the offer to all clients
          socketInstance.emit('offer', {
            offer
          });
          pc.setLocalDescription(offer);
        });
    });
  };  
  
  return (
    <View>
      <Text>Video Call Screen</Text>
      {localStream && <RTCView streamURL={localStream.toURL()} zOrder={1} style={{ width: 200, height: 150 }} />}
      {remoteStream && <RTCView streamURL={remoteStream.toURL()} zOrder={2} style={{ width: 200, height: 150 }} />}
      <Button title="Call" onPress={handleCall} />
    </View>
  );
};

export default VideoCall;

日志信息

LOG  rn-webrtc:pc:DEBUG 0 ctor +0ms
 LOG  rn-webrtc:pc:DEBUG 0 addTrack +4ms
 LOG  rn-webrtc:pc:DEBUG 0 addTrack +54ms
 LOG  rn-webrtc:pc:DEBUG 0 createOffer +12s
 LOG  rn-webrtc:pc:DEBUG 0 createOffer OK +117ms
 LOG  rn-webrtc:pc:DEBUG 0 setLocalDescription +4ms
 LOG  offerHandle Called
 LOG  rn-webrtc:pc:DEBUG 1 ctor +159ms
 LOG  rn-webrtc:pc:DEBUG 1 addTrack +216ms
 LOG  rn-webrtc:pc:DEBUG 1 addTrack +61ms
 LOG  rn-webrtc:pc:DEBUG 1 setRemoteDescription +52ms
 LOG  rn-webrtc:pc:DEBUG 0 setLocalDescription OK +25ms
 LOG  rn-webrtc:pc:DEBUG 1 ontrack +204ms
 LOG  rn-webrtc:pc:DEBUG 1 ontrack +82ms
 LOG  Received tracks: [{"_constraints": {}, "_enabled": true, "_muted": false, "_peerConnectionId": 1, "_readyState": "live", "_settings": {}, "id": "73d50db6-9154-4fd4-b274-c6c94f3ed3da", "kind": "audio", "label": "", "remote": true}]
 LOG  Received tracks: [{"_constraints": {}, "_enabled": true, "_muted": false, "_peerConnectionId": 1, "_readyState": "live", "_settings": {}, "id": "73d50db6-9154-4fd4-b274-c6c94f3ed3da", "kind": "audio", "label": "", "remote": true}, {"_constraints": {}, "_enabled": true, "_muted": false, "_peerConnectionId": 1, "_readyState": "live", "_settings": {}, "id": "5e19c8a9-23be-48d9-b0c6-3502b8323554", "kind": "video", "label": "", "remote": true}]
 LOG  rn-webrtc:pc:DEBUG 1 setRemoteDescription OK +93ms
 LOG  rn-webrtc:pc:DEBUG 1 createAnswer +2ms
 LOG  remote stream inUseeffect:  {"_id": "cdf55a01-3732-474e-a936-02b619d44aed", "_reactTag": "aeefb386-5ce1-44af-8050-77b02b097615", "_tracks": [{"_constraints": [Object], "_enabled": true, "_muted": false, "_peerConnectionId": 1, "_readyState": "live", "_settings": [Object], "id": "73d50db6-9154-4fd4-b274-c6c94f3ed3da", "kind": "audio", "label": "", "remote": true}, {"_constraints": [Object], "_enabled": true, "_muted": false, "_peerConnectionId": 1, "_readyState": "live", "_settings": [Object], "id": "5e19c8a9-23be-48d9-b0c6-3502b8323554", "kind": "video", "label": "", "remote": true}]}

问题分析与解决方案

从日志看,远程音视频track都已接收且状态为live,说明STUN服务器大概率没问题——ICE连接已经建立,track能正常接收。问题出在其他环节:

1. 缺失ICE Candidate处理(核心问题)

代码只实现了发送ICE Candidate,完全没处理对方发来的Candidate!这会导致媒体通道无法完全建立,虽然能收到track,但实际没有视频数据传输。

补充以下代码:

useEffect(() => {
  if (localStream) {
    socketInstance.on('ice-candidate', data => {
      peerConnections.forEach(pc => {
        pc.addIceCandidate(data.candidate).catch(err => console.error('添加ICE Candidate失败:', err));
      });
    });
  }
}, [localStream, peerConnections]);

2. PeerConnection实例管理混乱

一对一通话场景下,你创建了多个PC实例(localStream初始化时创建一个,收到offer又创建一个),会引发流冲突、资源浪费。建议维护单个PC实例,而非数组:

  • 把peerConnections改成单个状态变量peerConnection
  • 组件卸载时调用peerConnection?.close()清理资源

3. RTCView显示属性问题

  • 尝试给RTCView明确设置objectFit="contain"或cover,同时确保宽高样式生效
  • 可以尝试用streamId={remoteStream.id}替代streamURL={remoteStream.toURL()},部分RN-WebRTC版本对streamId支持更稳定
  • 检查zOrder是否导致远程流被遮挡(当前设置zOrder=2没问题,但可以临时调整验证)

4. 流状态验证

在ontrack事件里添加视频track的细节打印,确认视频参数正常:

pc.ontrack = event => {
  if (event.track.kind === 'video') {
    console.log('视频track参数:', event.track.getSettings());
    setRemoteStream(event.streams[0]);
  }
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 08:05:59