1

我一直在尝试在 React-Native 应用程序中进行视频通话。目前正在使用react-native-webrtc,这是此类项目的主流库。

我对此很陌生,但是基于 p2p 视频的最小示例(在此处找到),我编写了一个代码,试图让它们与不同的网络一起工作。该示例在一个流媒体与一个接收者之间创建了一个连接,但两者都在同一页面执行、同一网络、相同的所有内容上。

在我的情况下,我需要两个用户从不同的网络流式传输视频和接收视频。问题是,我找不到合适的地方来阅读和理解谈判在这种情况下的实际运作方式。

代码示例:

/**
 * @format
 * @flow
 */

import React, { useEffect } from 'react';
import firebase from '../firebase.config';
import { useSelector } from 'react-redux';
import {
    View,
    SafeAreaView,
    Button,
    StyleSheet,
    Dimensions,
    Text,
} from 'react-native';
import { RTCPeerConnection, RTCView, mediaDevices } from 'react-native-webrtc';
import store from '../redux/store';
import { Actions } from 'react-native-router-flux';
import { User } from 'models';
const oUserService = new User().getService(firebase);
const oCurrentReceivedStreamingService = new User().getService(
    firebase,
    store,
    'currentReceivedStreaming',
);

const viewport = Dimensions.get('window');

const Streaming = {
    call: (caller, receiver, localDescription) => {
        return {
            status: 'pending',
            users: {
                caller: {
                    uid: caller.uid,
                    localDescription,
                },
                receiver: {
                    uid: receiver.uid,
                    localDescription: '',
                },
            },
        };
    },
    answer: (receiver, localDescription) => {
        return {
            ...receiver.streaming,
            status: 'ongoing',
            users: {
                ...receiver.streaming.users,
                receiver: {
                    ...receiver.streaming.users.receiver,
                    localDescription,
                },
            },
        };
    },
    close: streaming => {
        return {
            ...streaming,
            status: 'closed',
        };
    },
};

const configuration = {
    iceServers: [
        { url: 'stun:stun.l.google.com:19302' },
        // { url: 'stun:stun1.l.google.com:19302' },
        // { url: 'stun:stun2.l.google.com:19302' },
        // { url: 'stun:stun3.l.google.com:19302' },
        // { url: 'stun:stun4.l.google.com:19302' },
        // { url: 'stun:stun.ekiga.net' },
        // { url: 'stun:stun.ideasip.com' },
        // { url: 'stun:stun.iptel.org' },
        // { url: 'stun:stun.rixtelecom.se' },
        // { url: 'stun:stun.schlund.de' },
        // { url: 'stun:stunserver.org' },
        // { url: 'stun:stun.softjoys.com' },
        // { url: 'stun:stun.voiparound.com' },
        // { url: 'stun:stun.voipbuster.com' },
        // { url: 'stun:stun.voipstunt.com' },
    ],
};

export default function App({ user, receiver, caller, session }) {
    const currentUserStore = useSelector(s => s.currentUserStore);
    const userStreamingStore = useSelector(s => s.userStreamingStore);
    const currentReceivedStreaming = useSelector(
        s => s.currentReceivedStreaming,
    );

    const [localStream, setLocalStream] = React.useState();
    const [remoteStream, setRemoteStream] = React.useState();
    const [cachedLocalPC, setCachedLocalPC] = React.useState();
    const [cachedRemotePC, setCachedRemotePC] = React.useState();

    useEffect(() => {
        oCurrentReceivedStreamingService.get(caller.uid);
    }, [receiver, caller, user, session]);

    let localPC, remotePC;

    const startLocalStream = async () => {
        const isFront = true;
        const devices = await mediaDevices.enumerateDevices();

        const facing = isFront ? 'front' : 'back';
        const videoSourceId = devices.find(
            device => device.kind === 'videoinput' && device.facing === facing,
        );
        const facingMode = isFront ? 'user' : 'environment';
        const constraints = {
            audio: true,
            video: {
                mandatory: {
                    minWidth: (viewport.height - 100) / 2,
                    minHeight: (viewport.height - 100) / 2,
                    minFrameRate: 30,
                },
                facingMode,
                optional: videoSourceId ? [{ sourceId: videoSourceId }] : [],
            },
        };
        const newStream = await mediaDevices.getUserMedia(constraints);
        setLocalStream(newStream);
        return Promise.resolve(newStream);
    };

    const startCall = async () => {
        try {
            let newStream = await startLocalStream();
            oCurrentReceivedStreamingService.get(session.user.uid);

            localPC = new RTCPeerConnection(configuration);
            remotePC = new RTCPeerConnection(configuration);

            localPC.onicecandidate = e => {
                try {
                    if (e.candidate) {
                        remotePC.addIceCandidate(e.candidate);
                    }
                } catch (err) {
                    console.error(`Error adding remotePC iceCandidate: ${err}`);
                }
            };
            remotePC.onicecandidate = e => {
                try {
                    if (e.candidate) {
                        localPC.addIceCandidate(e.candidate);
                    }
                } catch (err) {
                    console.error(`Error adding localPC iceCandidate: ${err}`);
                }
            };
            remotePC.onaddstream = e => {
                if (e.stream && remoteStream !== e.stream) {
                    setRemoteStream(e.stream);
                }
            };

            localPC.addStream(newStream);

            const offer = await localPC.createOffer();
            await localPC.setLocalDescription(offer);

            oUserService.patch(currentReceivedStreaming.current.uid, {
                streaming: Streaming.call(
                    currentReceivedStreaming.current,
                    user,
                    localPC.localDescription,
                ),
            });
        } catch (err) {
            console.error(err);
        }
        setCachedLocalPC(localPC);
        setCachedRemotePC(remotePC);
    };

    const answerCall = async (oUser, oCaller) => {
        try {
            let newStream = await startLocalStream();

            localPC = new RTCPeerConnection(configuration);
            remotePC = new RTCPeerConnection(configuration);

            localPC.onicecandidate = e => {
                try {
                    if (e.candidate) {
                        remotePC.addIceCandidate(e.candidate);
                    }
                } catch (err) {
                    console.error(`Error adding remotePC iceCandidate: ${err}`);
                }
            };
            remotePC.onicecandidate = e => {
                try {
                    if (e.candidate) {
                        localPC.addIceCandidate(e.candidate);
                    }
                } catch (err) {
                    console.error(`Error adding localPC iceCandidate: ${err}`);
                }
            };
            remotePC.onaddstream = e => {
                if (e.stream && remoteStream !== e.stream) {
                    setRemoteStream(e.stream);
                }
            };

            localPC.addStream(newStream);

            await remotePC.setRemoteDescription(oCaller.localDescription);

            let remoteStreams = remotePC.getRemoteStreams();
            remoteStreams.map(s => {
                console.log(s);
                setRemoteStream(s);
            });

            await localPC.setRemoteDescription(oCaller.localDescription);

            const offer = await localPC.createOffer();
            // const offer = await localPC.createAnswer();
            await localPC.setLocalDescription(offer);

            oUserService.patch(currentReceivedStreaming.current.uid, {
                streaming: Streaming.answer(
                    currentReceivedStreaming.current,
                    localPC.localDescription,
                ),
            });
        } catch (err) {
            console.error(err);
        }
        setCachedLocalPC(localPC);
        setCachedRemotePC(remotePC);
    };

    useEffect(() => {
        if (currentReceivedStreaming.current.uid) {
            let current = currentReceivedStreaming.current;
            if (current.streaming) {
                if (
                    current.streaming.status === 'closed' ||
                    current.streaming.status === 'rejected'
                ) {
                    // Actions.popTo('dashboard');
                }
                if (current.streaming.status === 'pending') {
                    if (
                        current.streaming.users.receiver.uid ===
                        session.user.uid
                    ) {
                        answerCall(current, current.streaming.users.caller);
                    }
                }
                if (current.streaming.status === 'ongoing' && remotePC) {
                    if (
                        current.streaming.users.caller.uid === session.user.uid
                    ) {
                        remotePC.setRemoteDescription(
                            current.streaming.receiver.localDescription,
                        );
                    }
                }
            }
        }
    }, [currentReceivedStreaming.current]);

    const closeStreams = () => {
        try {
            if (cachedLocalPC) {
                cachedLocalPC.removeStream(localStream);
                cachedLocalPC.close();
            }
            if (cachedRemotePC) {
                cachedRemotePC.removeStream(remoteStream);
                cachedRemotePC.close();
            }

            setLocalStream();
            setRemoteStream();
            setCachedRemotePC();
            setCachedLocalPC();

            oUserService
                .patch(currentReceivedStreaming.current.uid, {
                    streaming: {
                        ...currentReceivedStreaming.current.streaming,
                        status: 'closed',
                    },
                })
                .then(() => Actions.popTo('dashboard'));
        } catch (e) {
            console.log('ERROR', e);
        }
    };

    useEffect(() => {
        if (!localStream && caller.uid === session.user.uid) {
            startCall();
        }
    }, [currentUserStore.current.streaming]);

    return (
        <SafeAreaView style={styles.container}>
            {/* {!localStream && (
                <Button
                    title="Click to start stream"
                    onPress={startLocalStream}
                />
            )} */}
            {/* {localStream && (
                <Button
                    title="Click to start call"
                    onPress={startCall}
                    disabled={!!remoteStream}
                />
            )} */}

            <View style={styles.rtcview}>
                {localStream && (
                    <RTCView
                        style={styles.rtc}
                        streamURL={localStream.toURL()}
                    />
                )}
            </View>
            <Text>{!!remoteStream && 'YES'}</Text>
            <View style={styles.rtcview}>
                {remoteStream && (
                    <RTCView
                        style={styles.rtc}
                        streamURL={remoteStream.toURL()}
                    />
                )}
            </View>
            <Button title="Click to stop call" onPress={closeStreams} />
        </SafeAreaView>
    );
}

const styles = StyleSheet.create({
    container: {
        backgroundColor: '#313131',
        justifyContent: 'space-between',
        alignItems: 'center',
        height: '100%',
        paddingVertical: 30,
    },
    text: {
        fontSize: 30,
    },
    rtcview: {
        justifyContent: 'center',
        alignItems: 'center',
        height: '40%',
        width: '80%',
        backgroundColor: 'black',
        borderRadius: 10,
    },
    rtc: {
        width: '80%',
        height: '100%',
    },
});
4

1 回答 1

2

简而言之,从开发人员的角度来看,两个浏览器之间的视频通话是什么样的?

  1. 在第一个浏览器上完成所有必要的 JavaScript 对象的初步准备和创建之后,调用 WebRTC 方法 createOffer(),它返回一个 SDP 格式的文本包(或者,如果 oRTC 版本为API选择了“经典”之一)。该数据包包含有关开发人员想要什么样的通信的信息:语音、视频或发送数据,那里有哪些编解码器——这就是整个故事。
  2. 现在 - 信号。开发人员必须以某种方式(真的,它写在规范中!)将此文本包报价传递给第二个浏览器。例如,在 Internet 上使用您自己的服务器和来自两个浏览器的 WebSocket 连接。
  3. 在第二个浏览器上收到报价后,开发人员使用 setRemoteDescription() 方法将其传递给 WebRTC。然后他调用 createAnswer() 方法,该方法以 SDP 格式返回相同的文本数据包,但针对第二个浏览器并考虑到从第一个浏览器接收到的数据包。
  4. 信令继续:开发人员将应答文本包传回第一个浏览器。
  5. 在第一个浏览器上收到答案后,开发人员使用已经提到的 setRemoteDescription() 方法将其传递给 WebRTC,之后两个浏览器中的 WebRTC 彼此之间的感知最小。我可以连接吗?抱歉不行。其实,一切才刚刚开始……
  6. 两种浏览器中的WebRTC都开始分析网络连接的状态(其实标准并没有说明什么时候做这个,而且对于很多浏览器WebRTC在创建了相应的对象之后就立即开始研究网络,以免创建不必要的连接时延迟)。第一步开发者在创建WebRTC对象时,至少应该传递STUN服务器的地址。这是一个服务器,响应 UDP 数据包“我的 IP 是什么”,传输接收到该数据包的 IP 地址。WebRTC 使用 STUN 服务器获取“外部”IP 地址,将其与“内部”IP 地址进行比较,看看是否有 NAT。如果是这样,NAT 使用哪些反向端口来路由 UDP 数据包?
  7. 有时,两个浏览器上的 WebRTC 都会调用 onececandidate 回调,为第二个连接参与者传输带有信息的 SIP 数据包。此数据包包含有关内部和外部 IP 地址、连接尝试、NAT 使用的端口等信息。开发人员使用信令在浏览器之间传输这些数据包。使用 addIceCandidate() 方法将传输的数据包发送到 WebRTC。
  8. 一段时间后,WebRTC 将建立点对点连接。或者如果 NAT 会干扰将无法。对于这种情况,开发者可以传输 TURN 服务器的地址,该地址将用作外部连接元素:两个浏览器都会通过它传输带有语音或视频的 UDP 数据包。如果 STUN 服务器可以免费找到(例如,google 有),那么您将不得不自己提升 TURN 服务器。没有人对免费通过自己传递 TB 的视频流量感兴趣。

总之:您将需要最小的 STUN 服务器,作为最大的 TURN 服务器。或者两者兼有,如果您不知道用户将使用哪种网络配置。您可以阅读此博客以获取更多详细信息。我觉得它非常有用。

于 2020-01-13T15:47:57.700 回答