В этом руководстве мы изучим основы WebRTC для создания приложения для видеозвонков React Native, которое можно реализовать на iOS и Android.

Видеоконференцсвязь является важной частью современной среды. Однако из-за его сложности у большинства разработчиков (и у меня тоже 😅) возникают трудности с его реализацией.

WebRTC React Native — отличные фреймворки для создания приложений для видеоконференций. мы углубимся в эти фреймворки и разработаем одно приложение.

Если вам не терпится увидеть результаты, вот весь репозиторий react-native-webrtc-app для вашего проекта.

Что такое React Native?

React Native — это JavaScript-фреймворк для создания собственных мобильных приложений для iOS и Android. Он построен на React, наборе инструментов JavaScript Facebook для создания пользовательских интерфейсов, но предназначен не для браузеров, а для мобильных платформ. Другими словами, веб-разработчики теперь могут создавать мобильные приложения, которые выглядят и чувствуют себя полностью нативными, используя при этом уже знакомый им JavaScript-фреймворк. Кроме того, поскольку большую часть кода, который вы создаете, можно использовать на разных платформах, React Native упрощает одновременную сборку как для Android, так и для iOS.

Что такое WebRTC?

WebRTC (Web Real-Time Communications) — это протоколы P2P с открытым исходным кодом, которые позволяют веб-браузерам и устройствам общаться в режиме реального времени с помощью голоса, текста и видео. WebRTC предоставляет разработчикам программного обеспечения интерфейсы прикладного программирования (API), определенные в JavaScript.

P2P просто подразумевает, что два одноранговых узла (например, ваше устройство и мое) взаимодействуют друг с другом напрямую, без необходимости использования сервера посередине.

WebRTC использует ряд технологий для обеспечения одноранговой связи между браузерами в режиме реального времени.

  1. SDP (протокол описания сеанса)
  2. ICE (установление интерактивного соединения)
  3. RTP (протокол реального времени)

Еще один компонент, необходимый для запуска WebRTC, — это Signaling Server. Однако не существует стандарта для реализации сервера сигнализации, и его реализация может варьироваться от разработчика к разработчику. Более подробная информация о Signaling Server будет предоставлена ​​позже в этом разделе.

Давайте быстро пройдемся по некоторым технологиям, упомянутым выше.

SDP (протокол описания сеанса)

  • SDP — это простой протокол, который используется для определения того, какие кодеки поддерживаются браузерами. Предположим, что есть два одноранговых узла (клиент A и клиент B), которые будут подключены через WebRTC.
  • Клиенты A и B генерируют строки SDP, указывающие, какие кодеки они поддерживают. Например, клиент A может поддерживать видеокодеки H264, VP8 и VP9, ​​а также аудиокодеки Opus и PCM. Клиент B может поддерживать только H264 для видео и кодек Opus для аудио.
  • В этом сценарии между клиентом A и клиентом B будут использоваться кодеки H264 и Opus. Одноранговая связь не может быть установлена, если нет общих кодеков.

У вас может возникнуть вопрос о том, как эти строки SDP взаимодействуют друг с другом. Здесь мы будем использовать Signaling Server.

ICE (установление интерактивного соединения)

ICE — это волшебство, которое соединяет одноранговые узлы, даже если они разделены NAT.

  • Клиент A использует STUN-сервер для определения своих локальных и общедоступных интернет-адресов, которые они затем передают клиенту B через Signaling Server. Каждый адрес, полученный от сервера STUN, называется кандидатом ICE.

  • На изображении выше два сервера. Один из них — сервер STUN, а другой — сервер TURN.

STUN (утилиты обхода сеанса для NAT)

  • Сервер STUN используется для того, чтобы клиент A мог обнаружить все свои адреса.
  • Серверы STUN раскрывают общедоступные и локальные IP-адреса своих одноранговых узлов. Кстати, Google предлагает бесплатный сервер STUN (stun.l.google.com:19302).

TURN (Обход NAT с помощью реле)

  • Когда одноранговые соединения не могут быть сформированы, используется TURN Server. Сервер TURN просто передает данные между узлами.

RTP (протокол реального времени)

  • RTP — это хорошо зарекомендовавший себя стандарт для передачи данных в реальном времени. Он построен на UDP. В WebRTC аудио и видео передаются по RTP.

Сигнализация WebRTC

  • WebRTC может работать без серверов, но для создания соединения требуется сервер. Сервер служит каналом обмена информацией, необходимой для построения однорангового соединения.
  • Данные, которые должны быть переданы, это Offer, Answer и information about the Network Connection.

Давайте разберемся на примере.

  • Клиент А, который будет инициатором соединения, создаст предложение. Затем это предложение будет отправлено клиенту B через сигнальный сервер. Клиент B получит предложение и ответит соответствующим образом. Впоследствии эта информация будет передана Клиенту А по сигнальному каналу Клиентом Б.
  • Как только предложение и ответ завершены, устанавливается соединение между узлом и узлом. Кандидат ICE предоставляет протоколы и маршрутизацию, необходимые WebRTC для связи с удаленным устройством для этого однорангового обмена RTCIceCandidate. Каждый сверстник предлагает своих лучших кандидатов, от лучшего к худшему. Затем ссылка формируется после того, как они соглашаются использовать ее только один раз.

Создание сервера сигнализации Node js WebRTC

  • Теперь, когда мы рассмотрели основы WebRTC, давайте воспользуемся им для создания приложения для видеовызовов, использующего SocketIO в качестве канала сигнализации.
  • Как было сказано ранее, мы будем использовать WebRTC Node js SocketIO для передачи информации между клиентами.

Теперь мы создадим проект WebRTC Node js Express, и наша структура каталогов будет выглядеть примерно так.

server
    └── index.js
    └── socket.js
    └── package.json

Шаг 1:index.js файл будет выглядеть так.

const path = require('path');
const { createServer } = require('http');
const express = require('express');
const { getIO, initIO } = require('./socket');
const app = express();
app.use('/', express.static(path.join(__dirname, 'static')));
const httpServer = createServer(app);
let port = process.env.PORT || 3500;
initIO(httpServer);
httpServer.listen(port)
console.log("Server started on ", port);
getIO();

Шаг 2: socket.js файл будет выглядеть так.

const { Server } = require("socket.io");
let IO;
module.exports.initIO = (httpServer) => {
  IO = new Server(httpServer);
  IO.use((socket, next) => {
    if (socket.handshake.query) {
      let callerId = socket.handshake.query.callerId;
      socket.user = callerId;
      next();
    }
  });
  IO.on("connection", (socket) => {
    console.log(socket.user, "Connected");
    socket.join(socket.user);
    socket.on("call", (data) => {
      let calleeId = data.calleeId;
      let rtcMessage = data.rtcMessage;
      socket.to(calleeId).emit("newCall", {
        callerId: socket.user,
        rtcMessage: rtcMessage,
      });
    });
    socket.on("answerCall", (data) => {
      let callerId = data.callerId;
      rtcMessage = data.rtcMessage;
      socket.to(callerId).emit("callAnswered", {
        callee: socket.user,
        rtcMessage: rtcMessage,
      });
    });
    socket.on("ICEcandidate", (data) => {
      console.log("ICEcandidate data.calleeId", data.calleeId);
      let calleeId = data.calleeId;
      let rtcMessage = data.rtcMessage;
      socket.to(calleeId).emit("ICEcandidate", {
        sender: socket.user,
        rtcMessage: rtcMessage,
      });
    });
  });
};
module.exports.getIO = () => {
  if (!IO) {
    throw Error("IO not initilized.");
  } else {
    return IO;
  }
};

Как указывалось ранее, нам требуется, чтобы сервер передал три части информации: Offer, Answer и ICECandidate. Событие call отправляет вызывающему абоненту предложение вызывающего абонента, а событие answerCall отправляет вызывающему абоненту ответ вызываемого абонента. Событие ICEcandidate, обмен данными.

Это самая основная форма сервера сигнализации, которая нам требуется.

  1. package.json будет выглядеть так.

Шаг 3: файл package.json будет выглядеть так.

{
  "name": "WebRTC",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "socket.io": "^4.0.2" // Socket dependency
  }
}

Мы почти закончили программирование на стороне сервера. Давайте создадим клиентское приложение с React Native и WebRTC.

Прежде чем мы начнем разработку, давайте сначала поймем поток приложения. Мы будем предлагать CallerId всякий раз, когда пользователь открывает приложение (5-значное случайное число).

Например, у Джона и Мишеля есть CallerId, 12345 для Джона и 67890 для Мишеля, поэтому Джон инициирует вызов Мишеля со своим CallerId. Теперь Джон получит экран исходящего вызова, а Мишель увидит экран входящего вызова с кнопкой «Принять». Приняв вызов, Джон и Мишель присоединятся к встрече.

Разработать приложение React Native WebRTC

Шаг 1. Настройте собственный проект с помощью react-native-cli.

Вы можете следовать этому официальному руководству — https://reactnative.dev/docs/environment-setup

Шаг 2: После успешного запуска вашего демонстрационного приложения мы установим некоторую библиотеку React Native.

Вот мои зависимости package.json, которые вам также необходимо установить.

"dependencies": {
    "react": "17.0.2",
    "react-native": "0.68.2",
    "react-native-svg": "^13.7.0",
    "react-native-webrtc": "^1.94.2",
    "socket.io-client": "^4.5.4"
  }

Шаг 3. Настройка Android для пакета react-native-webrtc

Начиная с React Native 0.60 из-за новой функции автоматического связывания вам больше не нужно выполнять шаги по связыванию вручную, но вам нужно будет выполнить другие шаги, описанные ниже, если вы планируете выпустить свое приложение в рабочую среду.

3.1 Объявление разрешений

В android/app/main/AndroidManifest.xml добавьте следующие разрешения перед разделом <application>.

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.audio.output" />
<uses-feature android:name="android.hardware.microphone" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

3.2 Включить поддержку Java 8

В android/app/build.gradle добавьте следующее внутри раздела android.

compileOptions {
    sourceCompatibility JavaVersion.VERSIONOffer8
    targetCompatibility JavaVersion.VERSIONOffer8
}

3.3 Поддержка R8/ProGuard

В android/app/proguard-rules.pro добавьте следующее на новой строке.

-keep class org.webrtc.** { *; }

3.4 Неустранимое исключение: java.lang.UnsatisfiedLinkError

Fatal Exception: java.lang.UnsatisfiedLinkError: No implementation found for void org.webrtc.PeerConnectionFactory.nativeInitializeAndroidGlobals() (tried Java_org_webrtc_PeerConnectionFactory_nativeInitializeAndroidGlobals and Java_org_webrtc_PeerConnectionFactory_nativeInitializeAndroidGlobals__)

Если вы столкнулись с указанной выше ошибкой, в android/gradle.properties добавьте следующее.

android.enableDexingArtifactTransform.desugaring=false

Шаг 4: Настройка IOS для пакета react-native-webrtc

4.1 Настройка версии поддерживаемой платформы

ВАЖНО: убедитесь, что вы используете CocoaPods 1.10 или более позднюю версию.

Возможно, вам придется изменить поле platform в вашем подфайле.

react-native-webrtc не поддерживает iOS ‹ 12 Установите значение «12.0» или выше, иначе вы получите сообщение об ошибке при запуске pod install.

platform :ios, '12.0'

4.2 Объявление разрешений

Перейдите к <ProjectFolder>/ios/<ProjectName>/ и отредактируйте Info.plist, добавьте следующие строки.

<key>NSCameraUsageDescription</key>
<string>Camera permission description</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone permission description</string>

Шаг 5: Разработайте экраны пользовательского интерфейса

Наша структура каталогов для клиентского проекта будет выглядеть примерно так.

client
    └── android
    └── asset
    └── ios
    └── index.js
    └── App.js
    └── components
    └── package.json

Теперь мы разработаем JoinScreen , IncomingCallScreen и OutgoingCallScreen.

Мы будем использовать файл App.js на протяжении всего процесса разработки.

Присоединиться к экрану

import React, {useEffect, useState, useRef} from 'react';
import {
  Platform,
  KeyboardAvoidingView,
  TouchableWithoutFeedback,
  Keyboard,
  View,
  Text,
  TouchableOpacity,
} from 'react-native';
import TextInputContainer from './src/components/TextInputContainer';

export default function App({}) {
  const [type, setType] = useState('JOIN');
  const [callerId] = useState(
    Math.floor(100000 + Math.random() * 900000).toString(),
  );
  const otherUserId = useRef(null);

  const JoinScreen = () => {
    return (
      <KeyboardAvoidingView
        behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
        style={{
          flex: 1,
          backgroundColor: '#050A0E',
          justifyContent: 'center',
          paddingHorizontal: 42,
        }}>
        <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
          <>
            <View
              style={{
                padding: 35,
                backgroundColor: '#1A1C22',
                justifyContent: 'center',
                alignItems: 'center',
                borderRadius: 14,
              }}>
              <Text
                style={{
                  fontSize: 18,
                  color: '#D0D4DD',
                }}>
                Your Caller ID
              </Text>
              <View
                style={{
                  flexDirection: 'row',
                  marginTop: 12,
                  alignItems: 'center',
                }}>
                <Text
                  style={{
                    fontSize: 32,
                    color: '#ffff',
                    letterSpacing: 6,
                  }}>
                  {callerId}
                </Text>
              </View>
            </View>
            <View
              style={{
                backgroundColor: '#1A1C22',
                padding: 40,
                marginTop: 25,
                justifyContent: 'center',
                borderRadius: 14,
              }}>
              <Text
                style={{
                  fontSize: 18,
                  color: '#D0D4DD',
                }}>
                Enter call id of another user
              </Text>
              <TextInputContainer
                placeholder={'Enter Caller ID'}
                value={otherUserId.current}
                setValue={text => {
                  otherUserId.current = text;
                }}
                keyboardType={'number-pad'}
              />
              <TouchableOpacity
                onPress={() => {
                  setType('OUTGOING_CALL');
                }}
                style={{
                  height: 50,
                  backgroundColor: '#5568FE',
                  justifyContent: 'center',
                  alignItems: 'center',
                  borderRadius: 12,
                  marginTop: 16,
                }}>
                <Text
                  style={{
                    fontSize: 16,
                    color: '#FFFFFF',
                  }}>
                  Call Now
                </Text>
              </TouchableOpacity>
            </View>
          </>
        </TouchableWithoutFeedback>
      </KeyboardAvoidingView>
    );
  };
  const OutgoingCallScreen = () => {
    return null
  };
  const IncomingCallScreen = () => {
    return null
  };
  switch (type) {
    case 'JOIN':
      return JoinScreen();
    case 'INCOMING_CALL':
      return IncomingCallScreen();
    case 'OUTGOING_CALL':
      return OutgoingCallScreen();
    default:
      return null;
  }
}

Здесь, в приведенном выше файле, мы просто храним 5-значный случайный идентификатор вызывающего абонента, который будет представлять этого пользователя и может быть передан другим подключенным пользователем.

TextInputContainer.js файл кода компонента.

import React from 'react';
import {View, TextInput} from 'react-native';
const TextInputContainer = ({placeholder, value, setValue, keyboardType}) => {
  return (
    <View
      style={{
        height: 50,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#202427',
        borderRadius: 12,
        marginVertical: 12,
      }}>
      <TextInput
        style={{
          margin: 8,
          padding: 8,
          width: '90%',
          textAlign: 'center',
          fontSize: 16,
          color: '#FFFFFF',
        }}
        multiline={true}
        numberOfLines={1}
        cursorColor={'#5568FE'}
        placeholder={placeholder}
        placeholderTextColor={'#9A9FA5'}
        onChangeText={text => {
          setValue(text);
        }}
        value={value}
        keyboardType={keyboardType}
      />
    </View>
  );
};

export default TextInputContainer;

Наш экран будет выглядеть так.

Экран входящего вызова

import CallAnswer from './asset/CallAnswer';

export default function App({}) {
    //
  const IncomingCallScreen = () => {
    return (
      <View
        style={{
          flex: 1,
          justifyContent: 'space-around',
          backgroundColor: '#050A0E',
        }}>
        <View
          style={{
            padding: 35,
            justifyContent: 'center',
            alignItems: 'center',
            borderRadius: 14,
          }}>
          <Text
            style={{
              fontSize: 36,
              marginTop: 12,
              color: '#ffff',
            }}>
            {otherUserId.current} is calling..
          </Text>
        </View>
        <View
          style={{
            justifyContent: 'center',
            alignItems: 'center',
          }}>
          <TouchableOpacity
            onPress={() => {
              setType('WEBRTC_ROOM');
            }}
            style={{
              backgroundColor: 'green',
              borderRadius: 30,
              height: 60,
              aspectRatio: 1,
              justifyContent: 'center',
              alignItems: 'center',
            }}>
            <CallAnswer height={28} fill={'#fff'} />
          </TouchableOpacity>
        </View>
      </View>
    );
  };
    //
  }

Вы можете получить SVG значка CallAnswer здесь Активы.

Наш экран будет выглядеть так.

Экран исходящего вызова

import CallEnd from './asset/CallEnd';
export default function App({}) {
    //
const OutgoingCallScreen = () => {
    return (
      <View
        style={{
          flex: 1,
          justifyContent: 'space-around',
          backgroundColor: '#050A0E',
        }}>
        <View
          style={{
            padding: 35,
            justifyContent: 'center',
            alignItems: 'center',
            borderRadius: 14,
          }}>
          <Text
            style={{
              fontSize: 16,
              color: '#D0D4DD',
            }}>
            Calling to...
          </Text>
          <Text
            style={{
              fontSize: 36,
              marginTop: 12,
              color: '#ffff',
              letterSpacing: 6,
            }}>
            {otherUserId.current}
          </Text>
        </View>
        <View
          style={{
            justifyContent: 'center',
            alignItems: 'center',
          }}>
          <TouchableOpacity
            onPress={() => {
              setType('JOIN');
              otherUserId.current = null;
            }}
            style={{
              backgroundColor: '#FF5D5D',
              borderRadius: 30,
              height: 60,
              aspectRatio: 1,
              justifyContent: 'center',
              alignItems: 'center',
            }}>
            <CallEnd width={50} height={12} />
          </TouchableOpacity>
        </View>
      </View>
    );
  };
    //
  }

Вы можете получить SVG значка CallEnd здесь Активы.

Наш экран будет выглядеть так.

Шаг 6: Настройте WebSocket и WebRTC

После создания пользовательского интерфейса давайте настроим одноранговое соединение Socket и WebRTC в одном файле (App.js).

import SocketIOClient from 'socket.io-client'; // import socket io
// import WebRTC 
import {
  mediaDevices,
  RTCPeerConnection,
  RTCView,
  RTCIceCandidate,
  RTCSessionDescription,
} from 'react-native-webrtc';
export default function App({}) {
// Stream of local user
const [localStream, setlocalStream] = useState(null);
/* When a call is connected, the video stream from the receiver is appended to this state in the stream*/
const [remoteStream, setRemoteStream] = useState(null);
// This establishes your WebSocket connection
const socket = SocketIOClient('http://192.168.1.10:3500', {
    transports: ['websocket'],
    query: {
        callerId, 
    /* We have generated this `callerId` in `JoinScreen` implementation */
    },
  });
 /* This creates an WebRTC Peer Connection, which will be used to set local/remote descriptions and offers. */
 const peerConnection = useRef(
    new RTCPeerConnection({
      iceServers: [
        {
          urls: 'stun:stun.l.google.com:19302',
        },
        {
          urls: 'stun:stun1.l.google.com:19302',
        },
        {
          urls: 'stun:stun2.l.google.com:19302',
        },
      ],
    }),
  );
    useEffect(() => {
    socket.on('newCall', data => {
     /* This event occurs whenever any peer wishes to establish a call with you. */
    });
    socket.on('callAnswered', data => {
      /* This event occurs whenever remote peer accept the call. */
    });
    socket.on('ICEcandidate', data => {
      /* This event is for exchangin Candidates. */
    });
    let isFront = false;
/*The MediaDevices interface allows you to access connected media inputs such as cameras and microphones. We ask the user for permission to access those media inputs by invoking the mediaDevices.getUserMedia() method. */
    mediaDevices.enumerateDevices().then(sourceInfos => {
      let videoSourceId;
      for (let i = 0; i < sourceInfos.length; i++) {
        const sourceInfo = sourceInfos[i];
        if (
          sourceInfo.kind == 'videoinput' &&
          sourceInfo.facing == (isFront ? 'user' : 'environment')
        ) {
          videoSourceId = sourceInfo.deviceId;
        }
      }

      mediaDevices
        .getUserMedia({
          audio: true,
          video: {
            mandatory: {
              minWidth: 500, // Provide your own width, height and frame rate here
              minHeight: 300,
              minFrameRate: 30,
            },
            facingMode: isFront ? 'user' : 'environment',
            optional: videoSourceId ? [{sourceId: videoSourceId}] : [],
          },
        })
        .then(stream => {
          // Get local stream!
          setlocalStream(stream);
          // setup stream listening
          peerConnection.current.addStream(stream);
        })
        .catch(error => {
          // Log error
        });
    });
    peerConnection.current.onaddstream = event => {
      setRemoteStream(event.stream);
    };
    // Setup ice handling
    peerConnection.current.onicecandidate = event => {
    };
    return () => {
      socket.off('newCall');
      socket.off('callAnswered');
      socket.off('ICEcandidate');
    };
  }, []);
}

Шаг 7: Установите вызов WebRTC

На этом этапе объясняется, как устанавливаются вызовы WebRTC между одноранговыми узлами.

let remoteRTCMessage = useRef(null);
useEffect(() => {
  socket.on("newCall", (data) => {
    remoteRTCMessage.current = data.rtcMessage;
    otherUserId.current = data.callerId;
    setType("INCOMING_CALL");
  });
  socket.on("callAnswered", (data) => {
    // 7. When Alice gets Bob's session description, she sets that as the remote description with `setRemoteDescription` method.
    remoteRTCMessage.current = data.rtcMessage;
    peerConnection.current.setRemoteDescription(
      new RTCSessionDescription(remoteRTCMessage.current)
    );
    setType("WEBRTC_ROOM");
  });
  socket.on("ICEcandidate", (data) => {
    let message = data.rtcMessage;
    // When Bob gets a candidate message from Alice, he calls `addIceCandidate` to add the candidate to the remote peer description.
    if (peerConnection.current) {
      peerConnection?.current
        .addIceCandidate(new RTCIceCandidate(message.candidate))
        .then((data) => {
          console.log("SUCCESS");
        })
        .catch((err) => {
          console.log("Error", err);
        });
    }
  });
  // Alice creates an RTCPeerConnection object with an `onicecandidate` handler, which runs when network candidates become available.
  peerConnection.current.onicecandidate = (event) => {
    if (event.candidate) {
      // Alice sends serialized candidate data to Bob using Socket
      sendICEcandidate({
        calleeId: otherUserId.current,
        rtcMessage: {
          label: event.candidate.sdpMLineIndex,
          id: event.candidate.sdpMid,
          candidate: event.candidate.candidate,
        },
      });
    } else {
      console.log("End of candidates.");
    }
  };
}, []);
async function processCall() {
  // 1. Alice runs the `createOffer` method for getting SDP.
  const sessionDescription = await peerConnection.current.createOffer();
  // 2. Alice sets the local description using `setLocalDescription`.
  await peerConnection.current.setLocalDescription(sessionDescription);
  // 3. Send this session description to Bob uisng socket
  sendCall({
    calleeId: otherUserId.current,
    rtcMessage: sessionDescription,
  });
}
async function processAccept() {
  // 4. Bob sets the description, Alice sent him as the remote description using `setRemoteDescription()`
  peerConnection.current.setRemoteDescription(
    new RTCSessionDescription(remoteRTCMessage.current)
  );
  // 5. Bob runs the `createAnswer` method
  const sessionDescription = await peerConnection.current.createAnswer();
  // 6. Bob sets that as the local description and sends it to Alice
  await peerConnection.current.setLocalDescription(sessionDescription);
  answerCall({
    callerId: otherUserId.current,
    rtcMessage: sessionDescription,
  });
}
function answerCall(data) {
  socket.emit("answerCall", data);
}
function sendCall(data) {
  socket.emit("call", data);
}
const JoinScreen = () => {
  return (
    /*
      ...
      ...
      ...
      */
    <TouchableOpacity
      onPress={() => {
        processCall();
        setType("OUTGOING_CALL");
      }}
      style={{
        height: 50,
        backgroundColor: "#5568FE",
        justifyContent: "center",
        alignItems: "center",
        borderRadius: 12,
        marginTop: 16,
      }}
    >
      <Text
        style={{
          fontSize: 16,
          color: "#FFFFFF",
        }}
      >
        Call Now
      </Text>
    </TouchableOpacity>
    /*
      ...
      ...
      ...
      */
  );
};
const IncomingCallScreen = () => {
    return (
      /*
      ...
      ...
      ...
      */
      <TouchableOpacity
        onPress={() => {
          processAccept();
          setType('WEBRTC_ROOM');
        }}
        style={{
          backgroundColor: 'green',
          borderRadius: 30,
          height: 60,
          aspectRatio: 1,
          justifyContent: 'center',
          alignItems: 'center',
        }}>
        <CallAnswer height={28} fill={'#fff'} />
      </TouchableOpacity>
      /*
      ...
      ...
      ...
      */
    );
  };

Шаг 8: Рендеринг локального и удаленного MediaStream

import MicOn from "./asset/MicOn";
import MicOff from "./asset/MicOff";
import VideoOn from "./asset/VideoOn";
import VideoOff from "./asset/VideoOff";
import CameraSwitch from "./asset/CameraSwitch";
import IconContainer from "./src/components/IconContainer";

export default function App({}) {
  // Handling Mic status
  const [localMicOn, setlocalMicOn] = useState(true);

// Handling Camera status
  const [localWebcamOn, setlocalWebcamOn] = useState(true);

// Switch Camera
  function switchCamera() {
    localStream.getVideoTracks().forEach((track) => {
      track._switchCamera();
    });
  }

// Enable/Disable Camera
  function toggleCamera() {
    localWebcamOn ? setlocalWebcamOn(false) : setlocalWebcamOn(true);
    localStream.getVideoTracks().forEach((track) => {
      localWebcamOn ? (track.enabled = false) : (track.enabled = true);
    });
  }
  // Enable/Disable Mic
  function toggleMic() {
    localMicOn ? setlocalMicOn(false) : setlocalMicOn(true);
    localStream.getAudioTracks().forEach((track) => {
      localMicOn ? (track.enabled = false) : (track.enabled = true);
    });
  }
  // Destroy WebRTC Connection
  function leave() {
    peerConnection.current.close();
    setlocalStream(null);
    setType("JOIN");
  }
  const WebrtcRoomScreen = () => {
    return (
      <View
        style={{
          flex: 1,
          backgroundColor: "#050A0E",
          paddingHorizontal: 12,
          paddingVertical: 12,
        }}
      >
        {localStream ? (
          <RTCView
            objectFit={"cover"}
            style={{ flex: 1, backgroundColor: "#050A0E" }}
            streamURL={localStream.toURL()}
          />
        ) : null}
        {remoteStream ? (
          <RTCView
            objectFit={"cover"}
            style={{
              flex: 1,
              backgroundColor: "#050A0E",
              marginTop: 8,
            }}
            streamURL={remoteStream.toURL()}
          />
        ) : null}
        <View
          style={{
            marginVertical: 12,
            flexDirection: "row",
            justifyContent: "space-evenly",
          }}
        >
          <IconContainer
            backgroundColor={"red"}
            onPress={() => {
              leave();
              setlocalStream(null);
            }}
            Icon={() => {
              return <CallEnd height={26} width={26} fill="#FFF" />;
            }}
          />
          <IconContainer
            style={{
              borderWidth: 1.5,
              borderColor: "#2B3034",
            }}
            backgroundColor={!localMicOn ? "#fff" : "transparent"}
            onPress={() => {
              toggleMic();
            }}
            Icon={() => {
              return localMicOn ? (
                <MicOn height={24} width={24} fill="#FFF" />
              ) : (
                <MicOff height={28} width={28} fill="#1D2939" />
              );
            }}
          />
          <IconContainer
            style={{
              borderWidth: 1.5,
              borderColor: "#2B3034",
            }}
            backgroundColor={!localWebcamOn ? "#fff" : "transparent"}
            onPress={() => {
              toggleCamera();
            }}
            Icon={() => {
              return localWebcamOn ? (
                <VideoOn height={24} width={24} fill="#FFF" />
              ) : (
                <VideoOff height={36} width={36} fill="#1D2939" />
              );
            }}
          />
          <IconContainer
            style={{
              borderWidth: 1.5,
              borderColor: "#2B3034",
            }}
            backgroundColor={"transparent"}
            onPress={() => {
              switchCamera();
            }}
            Icon={() => {
              return <CameraSwitch height={24} width={24} fill="#FFF" />;
            }}
          />
        </View>
      </View>
    );
  };
}

Вы можете получить SVG значка CameraSwitch, VideoOn и MicOn здесь Активы.

IconContainer.js файл кода компонента.

import React from 'react';
import {TouchableOpacity} from 'react-native';
const buttonStyle = {
  height: 50,
  aspectRatio: 1,
  justifyContent: 'center',
  alignItems: 'center',
};
const IconContainer = ({backgroundColor, onPress, Icon, style}) => {
  return (
    <TouchableOpacity
      onPress={onPress}
      style={{
        ...style,
        backgroundColor: backgroundColor ? backgroundColor : 'transparent',
        borderRadius: 30,
        height: 60,
        aspectRatio: 1,
        justifyContent: 'center',
        alignItems: 'center',
      }}>
      <Icon />
    </TouchableOpacity>
  );
};
export default IconContainer;

Ууу!! Наконец мы это сделали.

Шаг 9. Обработка маршрутизации аудио в WebRTC

Мы будем использовать стороннюю библиотеку React Native Incall-Manager (https://github.com/react-native-webrtc/react-native-incall-manager) для обработки всех пограничных случаев, связанных со звуком, во время видеоконференций.

useEffect(() => {
    InCallManager.start();
    InCallManager.setKeepScreenOn(true);
    InCallManager.setForceSpeakerphoneOn(true);return () => {
      
InCallManager.stop();
    };
  }, []);

Вы можете получить полный исходный код здесь. С помощью этого блога мы создали приложение WebRTC с сигнальным сервером. Мы можем использовать одноранговую связь с 2-3 людьми в одной комнате/собрании.

Интегрируйте WebRTC с React Native с помощью Video SDK

Video SDK — это наиболее удобная для разработчиков платформа для живого видео и аудио SDK. Video SDK значительно упрощает и ускоряет интеграцию живого видео и аудио в ваш проект React Native. Вы можете создать фирменный, настраиваемый и программируемый вызов и запустить его в кратчайшие сроки, написав всего несколько строк кода.

Кроме того, Video SDK предоставляет лучшие в своем классе модификации, предоставляя вам полный контроль над макетом и правами. Для улучшения работы можно использовать плагины, а сквозные журналы вызовов и данные о качестве можно получить непосредственно с панели инструментов Video SDK или через REST API. Этот объем данных позволяет разработчикам отлаживать любые проблемы, возникающие во время разговора, и улучшать их интеграцию для наилучшего обслуживания клиентов.

Кроме того, вы можете следовать этому краткому руководству Создание демонстрационного проекта React Native с помощью Video SDK. или начните с Пример кода.

Ресурсы

Не забудьте поделиться этой статьей вTwitter,Reddit,Linkedin,Хакерские новости

Первоначально опубликовано на https://videosdk.live15 января 2023 г..