React - WEBRT C - Peer to Peer - Видеозвонок - Кажется, не работает - PullRequest
0 голосов
/ 13 января 2020

Я пытаюсь сделать видеозвонок в приложении 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%',
    },
});

1 Ответ

1 голос
/ 13 января 2020

В двух словах, как выглядит видеовызов между двумя браузерами с точки зрения разработчика?

  1. После предварительной подготовки и создания необходимых JavaScript объектов на первом В браузере вызывается метод createOffer () WebRT C, который возвращает текстовый пакет в формате SDP (или, в будущем, сериализуемый объект JSON, если версия API oRT C обнаруживает «классический»). " один). Этот пакет содержит информацию о том, какую коммуникацию хочет получить разработчик: голос, видео или отправка данных, какие там кодеки - вся эта история.
  2. А теперь - сигнализация. Разработчик должен каким-то образом (действительно, это написано в спецификации!) Передать это предложение текстового пакета во второй браузер. Например, используя свой собственный сервер для подключения Inte rnet и WebSocket из обоих браузеров.
  3. После получения предложения во втором браузере разработчик передает его в WebRT C с помощью метода setRemoteDescription () , Затем он вызывает метод createAnswer (), который возвращает тот же текстовый пакет в формате SDP, но для второго браузера и с учетом полученного пакета из первого браузера.
  4. Сигнализация продолжается: разработчик передает ответ текстовый пакет обратно в первый браузер.
  5. Получив ответ в первом браузере, разработчик передает его в WebRT C, используя уже упомянутый метод setRemoteDescription (), после чего WebRT C в обоих браузерах. минимально осведомлены друг о друге. Могу ли я подключиться? К сожалению нет. На самом деле, все только начинается ...
  6. WebRT C в обоих браузерах начинает анализировать состояние сетевого подключения (фактически стандарт не указывает, когда это делать, и для многих браузеров WebRT C начинает изучать сеть сразу после создания соответствующих объектов, чтобы не создавать ненужных задержек при подключении). Когда разработчик на первом этапе создавал объекты WebRT C, он должен как минимум передать адрес сервера STUN. Это сервер, который в ответ на пакет UDP «что такое мой IP» передает IP-адрес, с которого был получен этот пакет. WebRT C использует сервер STUN для получения «внешнего» IP-адреса, сравните его с «внутренним» и посмотрите, есть ли NAT. И если да, то какие обратные порты использует NAT для маршрутизации пакетов UDP?
  7. Время от времени WebRT C в обоих браузерах будет вызывать обратный вызов onicecandidate, передавая пакет SIP с информацией для второго участника соединения. , Этот пакет содержит информацию о внутренних и внешних IP-адресах, попытках подключения, портах, используемых NAT, и так далее. Разработчик использует сигнализацию для передачи этих пакетов между браузерами. Переданный пакет отправляется в WebRT C с помощью метода addIceCandidate ().
  8. Через некоторое время WebRT C установит sh одноранговое соединение. Или не сможет, если NAT будет мешать. В таких случаях разработчик может передать адрес сервера TURN, который будет использоваться в качестве внешнего связующего элемента: оба браузера будут передавать через него пакеты UDP с голосом или видео. Если сервер STUN можно найти бесплатно (например, в Google), то вам придется самостоятельно поднять сервер TURN. Никто не заинтересован в том, чтобы передавать терабайты видео-трафика c через себя бесплатно.

В заключение: вам понадобится минимальный сервер STUN, как максимальный сервер TURN. Или и то, и другое, если вы не знаете, какая конфигурация сети будет использоваться пользователями. Вы можете прочитать этот блог для более подробной информации. Я нахожу это очень информативным.

...