首页 > 解决方案 > JSON 类型 NSMutableDictionary 不能转换为 .sdp 不能为空

问题描述

我是 webRTC 的新手,我正在尝试使用本教程创建一个具有视频通话功能的本机应用程序,以遵循https://dipanshkhandelwal.medium.com/video-calling-using-firebase-and-webrtc-14cc2d4afceb

但是,我在 iOS 和 android 上不断收到此错误,一旦我尝试加入通话,该应用程序就会关闭。我在 iOS 上遇到的错误说:

JSON value '{
}' of type NSMutableDictionary cannot be converted to .sdp must not be null

+[RCTConvert(WebRTC) RTCSessionDescription:]
    RCTConvert+WebRTC.m:22
__41-[RCTModuleMethod processMethodSignature]_block_invoke_16
-[RCTModuleMethod invokeWithBridge:module:arguments:]
facebook::react::invokeInner(RCTBridge*, RCTModuleData*, unsigned int, folly::dynamic const&, int, (anonymous namespace)::SchedulingContext)
facebook::react::RCTNativeModule::invoke(unsigned int, folly::dynamic&&, int)::$_0::operator()() const
invocation function for block in facebook::react::RCTNativeModule::invoke(unsigned int, folly::dynamic&&, int)
_dispatch_call_block_and_release
_dispatch_client_callout
_dispatch_lane_serial_drain
_dispatch_lane_invoke
_dispatch_workloop_worker_thread
_pthread_wqthread
start_wqthread

在地铁中,这是一个错误:

Possible Unhandled Promise Rejection (id: 0):
Object {
  "message": "SessionDescription is NULL.",
  "name": "SetRemoteDescriptionFailed",
}

下面是在相应房间的 firstore 中创建的报价示例。我怀疑 sdp 可能是无效的或什么的。

_sdp:

v=0o=- 765269967391877801 2 IN IP4 127.0.0.1s=-t=0 0a=group:BUNDLE audio videoa=msid-semantic: WMS a3a6a14c-980f-4052-a881-e53290c2e9a8m=video 100c=IN IP4 0.0.0.0a=rtcp:9 IN IP4 0.0.0.0a=ice-ufrag:qfEea=ice-pwd:aMtS7ouM3ykOCV6z7jx0hhlSa=ice-options:trickle renominationa=fingerprint:sha-256 01:13:79:2E:BA:E3:67:9A:77:66:90:00:D7:62:BA:31:2C:48:FC:EB:8C:21:22:7E:5B:9D:0E:71:82:5F:63:5Aa=setup:actpassa=mid:audioa=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-levela=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timea=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01a=sendrecva=rtcp-muxa=rtpmap:111 opus/48000/2a=rtcp-fb:111 transport-cca=fmtp:111 minptime=10;useinbandfec=1a=rtpmap:103 ISAC/16000a=rtpmap:9 G722/8000a=rtpmap:102 ILBC/8000a=rtpmap:0 PCMU/8000a=rtpmap:8 PCMA/8000a=rtpmap:105 CN/16000a=rtpmap:13 CN/8000a=rtpmap:110 telephone-event/48000a=rtpmap:113 telephone-event/16000a=rtpmap:126 telephone-event/8000a=ssrc:866776128 cname:OXEDL0qxAsrhPmB7a=ssrc:866776128 msid:a3a6a14c-980f-4052-a881-e53290c2e9a8 256a9ac0-8163-45b5-ae52-0ece88651c51a=ssrc:866776128 mslabel:a3a6a14c-980f-4052-a881-e53290c2e9a8a=ssrc:866776128 label:256a9ac0-8163-45b5-ae52-0ece88651c51m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127c=IN IP4 0.0.0.0a=rtcp:9 IN IP4 0.0.0.0a=ice-ufrag:qfEea=ice-pwd:aMtS7ouM3ykOCV6z7jx0hhlSa=ice-options:trickle renominationa=fingerprint:sha-256 01:13:79:2E:BA:E3:67:9A:77:66:90:00:D7:62:BA:31:2C:48:FC:EB:8C:21:22:7E:5B:9D:0E:71:82:5F:63:5Aa=setup:actpassa=mid:videoa=extmap:14 urn:ietf:params:rtp-hdrext:toffseta=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timea=extmap:13 urn:3gpp:video-orientationa=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delaya=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-typea=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timinga=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-spacea=sendrecva=rtcp-muxa=rtcp-rsizea=rtpmap:96 VP8/90000a=rtcp-fb:96 goog-remba=rtcp-fb:96 transport-cca=rtcp-fb:96 ccm fira=rtcp-fb:96 nacka=rtcp-fb:96 nack plia=rtpmap:97 rtx/90000a=fmtp:97 apt=96a=rtpmap:98 VP9/90000a=rtcp-fb:98 goog-remba=rtcp-fb:98 transport-cca=rtcp-fb:98 ccm fira=rtcp-fb:98 nacka=rtcp-fb:98 nack plia=rtpmap:99 rtx/90000a=fmtp:99 apt=98a=rtpmap:100 red/90000a=rtpmap:101 rtx/90000a=fmtp:101 apt=100a=rtpmap:127 ulpfec/90000a=ssrc-group:FID 2850853975 3259115858a=ssrc:2850853975 cname:OXEDL0qxAsrhPmB7a=ssrc:2850853975 msid:a3a6a14c-980f-4052-a881-e53290c2e9a8 3f3952f6-cd3d-4d3e-a2dc-81eda77cf516a=ssrc:2850853975 mslabel:a3a6a14c-980f-4052-a881-e53290c2e9a8a=ssrc:2850853975 label:3f3952f6-cd3d-4d3e-a2dc-81eda77cf516a=ssrc:3259115858 cname:OXEDL0qxAsrhPmB7a=ssrc:3259115858 msid:a3a6a14c-980f-4052-a881-e53290c2e9a8 3f3952f6-cd3d-4d3e-a2dc-81eda77cf516a=ssrc:3259115858 mslabel:a3a6a14c-980f-4052-a881-e53290c2e9a8a=ssrc:3259115858 label:3f3952f6-cd3d-4d3e-a2dc-81eda77cf516

尝试了一切,但似乎无法弄清楚。被调用者集合也永远不会在 firestore 数据库的各个房间中创建。对此的任何帮助将不胜感激。

标签: javascriptjsonfirebasereact-nativewebrtc

解决方案


我猜您正在尝试使用 firebase 作为信号媒介,并希望使用 react-native-webrtc 进行视频通话。

这是我使用最新库和 react-native 版本的相同解决方案的示例代码。

Firebase 安装 React Native。

只需使用上面的链接设置 ios 和 android,然后使用下面的代码作为参考。

import React, {useEffect, useState} from 'react';

import {Modal, TextInput, ToastAndroid, View} from 'react-native';
import {ColoredButton} from '../app/common/commonViews';
import {
  mediaDevices,
  MediaStream,
  RTCIceCandidate,
  RTCPeerConnection,
  RTCSessionDescription,
  RTCView,
} from 'react-native-webrtc';

import firestore from '@react-native-firebase/firestore';

const Home = () => {
  const [localStream, setLocalStream] = useState(null);
  const [remoteStream, setRemoteStream] = useState(new MediaStream());
  const [modalVisible, setModalVisible] = useState(false);
  const [roomName, setRoomName] = useState('2277');
  let peerConnection;

  const configuration = {
    iceServers: [
      {urls: 'stun:stun.services.mozilla.com'},
      {urls: 'stun:stun.l.google.com:19302'},
    ],
  };
  let isFront = true;

  useEffect(() => {
    mediaDevices.enumerateDevices().then((sourceInfos) => {
      console.log(sourceInfos);
      let videoSourceId;
      for (let i = 0; i < sourceInfos.length; i++) {
        const sourceInfo = sourceInfos[i];
        if (
          sourceInfo.kind == 'videoinput' &&
          sourceInfo.facing == (isFront ? 'front' : 'environment')
        ) {
          videoSourceId = sourceInfo.deviceId;
        }
      }
      mediaDevices
        .getUserMedia({
          audio: true,
          video: {
            width: 640,
            height: 480,
            frameRate: 30,
            facingMode: isFront ? 'user' : 'environment',
            deviceId: videoSourceId,
          },
        })
        .then((stream) => {
          setLocalStream(stream);
          // Got stream!
        })
        .catch((error) => {
          // Log error
        });
    });
  }, [isFront]);

  async function createRoom() {
    const roomRef = firestore().collection('rooms').doc(roomName);
    console.log(roomRef);
    console.log('Create PeerConnection with configuration: ', configuration);
    peerConnection = new RTCPeerConnection(configuration);

    peerConnection.addStream(localStream);

    // Code for collecting ICE candidates below
    const callerCandidatesCollection = roomRef.collection('callerCandidates');

    // Code for collecting ICE candidates above

    peerConnection.onicecandidate = function (event) {
      if (!event.candidate) {
        console.log('Got final candidate!');
        return;
      }
      console.log('Got candidate: ', event.candidate);
      callerCandidatesCollection.add(event.candidate.toJSON());
    };

    peerConnection.onicegatheringstatechange = () => {
      console.log(
        `ICE gathering state changed: ${peerConnection.iceGatheringState}`,
      );
    };

    peerConnection.onconnectionstatechange = () => {
      console.log(`Connection state change: ${peerConnection.connectionState}`);
    };

    peerConnection.onsignalingstatechange = () => {
      console.log(`Signaling state change: ${peerConnection.signalingState}`);
    };

    peerConnection.oniceconnectionstatechange = () => {
      console.log(
        `ICE connection state change: ${peerConnection.iceConnectionState}`,
      );
    };

    console.log('Created offer:', offer);

    // Code for creating a room below
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);

    const roomWithOffer = {
      offer: {
        type: offer.type,
        sdp: offer.sdp,
      },
    };
    await roomRef.set(roomWithOffer);
    console.log(`New room created with SDP offer. Room ID: ${roomRef.id}`);
    ToastAndroid.show(
      `New room created with SDP offer. Room ID: ${roomRef.id}`,
      ToastAndroid.SHORT,
    );
    // Code for creating a room above

    peerConnection.onaddstream = (event) => {
      setRemoteStream(event.stream);
    };

    // Listening for remote session description below
    roomRef.onSnapshot(async (snapshot) => {
      const data = snapshot.data();
      if (!peerConnection.currentRemoteDescription && data && data.answer) {
        console.log('Got remote description: ', data.answer);
        const rtcSessionDescription = new RTCSessionDescription(data.answer);
        await peerConnection.setRemoteDescription(rtcSessionDescription);
      }
    });
    // Listening for remote session description above

    // Listen for remote ICE candidates below
    roomRef.collection('calleeCandidates').onSnapshot((snapshot) => {
      snapshot.docChanges().forEach(async (change) => {
        if (change.type === 'added') {
          let data = change.doc.data();
          console.log(`Got new remote ICE candidate: ${JSON.stringify(data)}`);
          await peerConnection.addIceCandidate(new RTCIceCandidate(data));
        }
      });
    });
    // Listen for remote ICE candidates above
  }

  async function joinRoomById(roomId) {
    const roomRef = firestore().collection('rooms').doc(`${roomId}`);
    const roomSnapshot = await roomRef.get({
      source: 'server',
    });
    console.log('Got room:', roomSnapshot.exists);

    if (roomSnapshot.exists) {
      console.log('Create PeerConnection with configuration: ', configuration);
      peerConnection = new RTCPeerConnection(configuration);
      peerConnection.addStream(localStream);

      // Code for collecting ICE candidates below
      const calleeCandidatesCollection = roomRef.collection('calleeCandidates');
      peerConnection.onicecandidate = function (event) {
        if (!event.candidate) {
          console.log('Got final candidate!');
          return;
        }
        console.log('Got candidate: ', event.candidate);
        calleeCandidatesCollection.add(event.candidate.toJSON());
      };

      peerConnection.onicegatheringstatechange = () => {
        console.log(
          `ICE gathering state changed: ${peerConnection.iceGatheringState}`,
        );
      };

      peerConnection.onconnectionstatechange = () => {
        console.log(
          `Connection state change: ${peerConnection.connectionState}`,
        );
      };

      peerConnection.onsignalingstatechange = () => {
        console.log(`Signaling state change: ${peerConnection.signalingState}`);
      };

      peerConnection.oniceconnectionstatechange = () => {
        console.log(
          `ICE connection state change: ${peerConnection.iceConnectionState}`,
        );
      };

      // Code for collecting ICE candidates above

      peerConnection.onaddstream = (event) => {
        setRemoteStream(event.stream);
      };

      // Code for creating SDP answer below
      const offer = roomSnapshot.data().offer;
      console.log('Got offer:', offer);
      await peerConnection.setRemoteDescription(
        new RTCSessionDescription(offer),
      );
      const answer = await peerConnection.createAnswer();
      console.log('Created answer:', answer);
      await peerConnection.setLocalDescription(answer);

      const roomWithAnswer = {
        answer: {
          type: answer.type,
          sdp: answer.sdp,
        },
      };
      await roomRef.update(roomWithAnswer);
      // Code for creating SDP answer above

      // Listening for remote ICE candidates below
      roomRef.collection('callerCandidates').onSnapshot((snapshot) => {
        snapshot.docChanges().forEach(async (change) => {
          if (change.type === 'added') {
            let data = change.doc.data();
            console.log(
              `Got new remote ICE candidate: ${JSON.stringify(data)}`,
            );
            await peerConnection.addIceCandidate(new RTCIceCandidate(data));
          }
        });
      });
      // Listening for remote ICE candidates above
    }
  }

  return (
    <View style={{flex: 1}}>
      <Modal
        visible={modalVisible}
        transparent={true}
        style={{justifyContent: 'center'}}>
        <View
          style={{
            height: 100,
            padding: 20,
            width: '80%',
            alignSelf: 'center',
            justifyContent: 'center',
            backgroundColor: 'white',
          }}>
          <TextInput
            value={roomName}
            onChangeText={(text) => {
              setRoomName(text);
            }}
            placeholder={'Enter text'}
          />
          <View
            style={{
              flexDirection: 'row',
              justifyContent: 'space-evenly',
            }}>
            <ColoredButton
              title="close"
              onPress={() => {
                setModalVisible(!modalVisible);
              }}
            />
            <ColoredButton
              title="create"
              onPress={() => {
                setModalVisible(!modalVisible);
              }}
            />
            <ColoredButton
              title="join"
              onPress={() => {
                setModalVisible(!modalVisible);
              }}
            />
          </View>
        </View>
      </Modal>
      <RTCView
        streamURL={localStream ? localStream.toURL() : null}
        style={{flex: 1, margin: 20, backgroundColor: 'black'}}
      />
      <RTCView
        style={{
          flex: 1,
          marginLeft: 20,
          marginRight: 20,
          backgroundColor: 'black',
        }}
        streamURL={remoteStream ? remoteStream.toURL() : null}
      />
      <TextInput value={roomName} onChangeText={setRoomName} />
      <View
        style={{
          flexDirection: 'row',
          justifyContent: 'space-evenly',
          width: '100%',
          padding: 20,
        }}>
        <ColoredButton
          title="Create"
          onPress={() => {
            createRoom().then((r) => {});
          }}
        />
        <ColoredButton
          title="Join"
          onPress={() => {
            joinRoomById(roomName).then((r) => {});
          }}
        />
      </View>
    </View>
  );
};

export default Home;

推荐阅读