كيفية إضافة مكالمات الفيديو إلى تطبيق React Native باستخدام Twilio

دقائق القراءة: 9

مقدمة

أصبحت مكالمات الفيديو جزءاً أساسياً من التواصل اليومي، خاصة مع الاعتماد المتزايد على الحلول الرقمية للعمل والتعلّم والتواصل الشخصي. وإذا كنت تطوّر تطبيقاً باستخدام React Native، فإن إضافة ميزة اتصال مرئي احترافية يمكن أن ترفع من قيمة التطبيق بشكل كبير.

في هذا الدليل العملي، سنشرح كيفية بناء ميزة مكالمات فيديو داخل تطبيق React Native بالاعتماد على واجهة Twilio Programmable Video API. الفكرة بسيطة: ننشئ غرفة اتصال، ثم نتيح للمستخدمين الانضمام إليها عبر رمز وصول آمن Access Token يتم توليده من خادم Node.js.

مكالمة فيديو داخل تطبيق React Native باستخدام Twilio

سنغطي في هذا المقال:

  • إنشاء مفاتيح Twilio API.
  • إعداد خادم Node.js لتوليد رمز الوصول.
  • تهيئة مشروع React Native على iOS وAndroid.
  • بناء شاشة التسجيل والانضمام إلى الغرفة.
  • إنشاء شاشة مكالمة الفيديو والتحكم في الميكروفون والكاميرا.

ولأن ميزة الفيديو تحتاج إلى الكاميرا والميكروفون، فمن الأفضل اختبار التطبيق على أجهزة حقيقية بدلاً من الاعتماد الكامل على المحاكيات.

المتطلبات الأساسية قبل البدء

  • حساب فعّال على Twilio.
  • جهازان على الأقل بنظام iOS أو Android لاختبار الاتصال بين مستخدمين.
  • بيئة تطوير React Native جاهزة.
  • فهم أساسي لـ JavaScript وNode.js.

كيفية الحصول على مفاتيح Twilio API

للبدء، تحتاج إلى إنشاء حساب على منصة Twilio. بعد تسجيل الدخول، انتقل إلى لوحة التحكم للحصول على بيانات الاعتماد اللازمة مثل:

  • ACCOUNT_SID
  • API_KEY_SID
  • API_KEY_SECRET

هذه القيم ستُستخدم لاحقاً في الخادم لتوليد رمز مصادقة آمن لكل مستخدم يرغب في الانضمام إلى غرفة الفيديو.

واجهة الحصول على مفاتيح Twilio API من لوحة التحكم

إعداد خادم Node.js لتوليد Access Token

حتى يتمكن التطبيق من الاتصال بخدمة الفيديو، نحتاج إلى خادم وسيط يقوم بتوليد Access Token لكل مستخدم. هذا النهج أكثر أماناً من وضع بيانات Twilio الحساسة مباشرة داخل تطبيق الهاتف.

تثبيت الحزم المطلوبة

ابدأ بإنشاء مشروع جديد، ثم ثبّت الحزم التالية:

yarn add dotenv express ngrok nodemon twilio

إضافة متغيرات البيئة

أنشئ ملف .env وأضف بيانات الاعتماد:

PORT=3000
ACCOUNT_SID=AC5ceb0847c50c91b143ce07
API_KEY_SID=SKa173c10de99a26fd86969b
API_KEY_SECRET=Czv7IjNIZJis8s7jb5FePi

يفضّل في المشاريع الحقيقية عدم مشاركة هذه البيانات علناً، والاعتماد على مفاتيح آمنة ومقيدة الصلاحيات.

إنشاء نقطة API لإرجاع الرمز

الآن نبدأ بتهيئة الخادم واستيراد الحزم الأساسية:

import 'dotenv/config';
import express from 'express';
import twilio from 'twilio';
import ngrok from 'ngrok';

const AccessToken = twilio.jwt.AccessToken;
const VideoGrant = AccessToken.VideoGrant;
const app = express();

بعد ذلك، ننشئ مساراً مثل /getToken يستقبل اسم المستخدم ويعيد رمز JWT صالحاً للاتصال:

app.get('/getToken', (req, res) => {
  if (!req.query || !req.query.userName) {
    return res.status(400).send('Username parameter is required');
  }

  const accessToken = new AccessToken(
    process.env.ACCOUNT_SID,
    process.env.API_KEY_SID,
    process.env.API_KEY_SECRET,
  );

  accessToken.identity = req.query.userName;

  var grant = new VideoGrant();
  accessToken.addGrant(grant);

  var jwt = accessToken.toJwt();
  return res.send(jwt);
});

في هذا المسار يحدث ما يلي:

  1. التحقق من وجود المتغير userName.
  2. إنشاء كائن من AccessToken.
  3. ربط هوية المستخدم بالحقل identity.
  4. إضافة صلاحية الفيديو عبر VideoGrant.
  5. إرجاع رمز JWT للتطبيق.

إتاحة الخادم للاختبار عبر الإنترنت

لأن الهاتف يحتاج للوصول إلى الخادم، يمكن استخدام ngrok لتوليد رابط عام:

app.listen(process.env.PORT, () =>
  console.log(`Server listening on port ${process.env.PORT}!`),
);

ngrok.connect(process.env.PORT).then((url) => {
  console.log(`Server forwarded to public url ${url}`);
});

بهذه الطريقة تحصل على رابط خارجي يمكن لتطبيق الهاتف استخدامه أثناء الاختبار.

تشغيل خادم Node.js وإنشاء رابط ngrok عام

تهيئة مشروع React Native لمكالمات الفيديو

في جانب التطبيق، سنعتمد على مكتبة react-native-twilio-video-webrtc لربط التطبيق بخدمة الفيديو، بالإضافة إلى مكتبات التنقل وإدارة الصلاحيات.

تثبيت الحزم الأساسية

yarn add @react-navigation/native @react-navigation/stack react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view react-native-dotenv react-native-permissions https://github.com/blackuy/react-native-twilio-video-webrtc

إعداد iOS

منصة Twilio Video SDK تتطلب دعماً لـ iOS 11.0+، لذلك يجب تحديث ملف Podfile كما يلي:

platform :ios, '11.0'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

إضافة صلاحيات الكاميرا والميكروفون

في ملف Podfile أضف إعدادات الصلاحيات:

permissions_path = '../node_modules/react-native-permissions/ios'
pod 'Permission-Camera', :path => "#{permissions_path}/Camera.podspec"
pod 'Permission-Microphone', :path => "#{permissions_path}/Microphone.podspec"

ثم افتح ملف info.plist وأضف رسائل طلب الإذن:

<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSCameraUsageDescription</key>
<string>We require your permission to access the camera while in a video call</string>
<key>NSMicrophoneUsageDescription</key>
<string>We require your permission to access the microphone while in a video call</string>

إعداد Android

على Android نحتاج إلى ربط المكتبة يدوياً داخل ملفات المشروع.

تعديل ملف settings.gradle

project(':react-native-twilio-video-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-twilio-video-webrtc/android')

تعديل ملف build.gradle

implementation project(':react-native-twilio-video-webrtc')

إضافة الحزمة داخل MainApplication.java

import com.twiliorn.library.TwilioPackage;

ثم فعّلها داخل الدالة getPackages():

@Override
protected List getPackages() {
  @SuppressWarnings("UnnecessaryLocalVariable")
  List packages = new PackageList(this).getPackages();
  packages.add(new TwilioPackage());
  return packages;
}

بناء شاشة تسجيل الغرفة في React Native

سننشئ شاشة تسمح للمستخدم بإدخال اسمه واسم الغرفة قبل بدء المكالمة. هذه الشاشة هي نقطة الانطلاق للحصول على الرمز من الخادم ثم الانتقال إلى شاشة الاتصال.

استيراد الحزم المطلوبة

import React, {useState, useRef, useEffect, useContext} from 'react';
import {
  StyleSheet,
  View,
  Text,
  StatusBar,
  TouchableOpacity,
  TextInput,
  Alert,
  KeyboardAvoidingView,
  Platform,
  ScrollView,
  Dimensions,
} from 'react-native';
import {
  TwilioVideoLocalView,
  TwilioVideoParticipantView,
  TwilioVideo,
} from 'react-native-twilio-video-webrtc';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';

وظيفة كل جزء باختصار:

  • react-navigation: للتنقل بين شاشة التسجيل وشاشة المكالمة.
  • react-native-permissions: لإدارة أذونات الكاميرا والميكروفون.
  • react-native-twilio-video-webrtc: للتعامل مع مكالمات الفيديو.

تهيئة الحالة العامة للتطبيق

const Stack = createStackNavigator();

const initialState = {
  isAudioEnabled: true,
  status: 'disconnected',
  participants: new Map(),
  videoTracks: new Map(),
  userName: '',
  roomName: '',
  token: '',
};

const AppContext = React.createContext(initialState);
const dimensions = Dimensions.get('window');

هذه الحالة تحتفظ ببيانات الجلسة الحالية مثل اسم المستخدم، اسم الغرفة، حالة الاتصال، ومسارات الفيديو.

إعداد التنقل وتوزيع الحالة عبر Context

export default () => {
  const [props, setProps] = useState(initialState);

  return (
    <>
      <StatusBar barStyle="dark-content" />
      <AppContext.Provider value={{props, setProps}}>
        <NavigationContainer>
          <Stack.Navigator>
            <Stack.Screen name="Home" component={HomeScreen} />
            <Stack.Screen name="Video Call" component={VideoCallScreen} />
          </Stack.Navigator>
        </NavigationContainer>
      </AppContext.Provider>
    </>
  );
};

إنشاء شاشة التسجيل والتحقق من الصلاحيات

في ملف شاشة التسجيل، نحتاج أولاً إلى الوصول إلى الحالة العامة:

import React, {useState, useRef, useEffect, useContext} from 'react';
import {
  checkMultiple,
  request,
  requestMultiple,
  PERMISSIONS,
  RESULTS,
} from 'react-native-permissions';

const RegisterScreen = ({navigation}) => {
  const {props, setProps} = useContext(AppContext);

فحص صلاحيات الكاميرا والميكروفون

قبل بدء المكالمة، يجب التأكد من منح التطبيق إذن الوصول إلى العتاد المطلوب:

const _checkPermissions = (callback) => {
  const iosPermissions = [PERMISSIONS.IOS.CAMERA, PERMISSIONS.IOS.MICROPHONE];
  const androidPermissions = [
    PERMISSIONS.ANDROID.CAMERA,
    PERMISSIONS.ANDROID.RECORD_AUDIO,
  ];

  checkMultiple(
    Platform.OS === 'ios' ? iosPermissions : androidPermissions,
  ).then((statuses) => {
    const [CAMERA, AUDIO] = Platform.OS === 'ios' ? iosPermissions : androidPermissions;

    if (
      statuses[CAMERA] === RESULTS.UNAVAILABLE ||
      statuses[AUDIO] === RESULTS.UNAVAILABLE
    ) {
      Alert.alert('Error', 'Hardware to support video calls is not available');
    } else if (
      statuses[CAMERA] === RESULTS.BLOCKED ||
      statuses[AUDIO] === RESULTS.BLOCKED
    ) {
      Alert.alert('Error', 'Permission to access hardware was blocked, please grant manually');
    } else {
      if (
        statuses[CAMERA] === RESULTS.DENIED &&
        statuses[AUDIO] === RESULTS.DENIED
      ) {
        requestMultiple(
          Platform.OS === 'ios' ? iosPermissions : androidPermissions,
        ).then((newStatuses) => {
          if (
            newStatuses[CAMERA] === RESULTS.GRANTED &&
            newStatuses[AUDIO] === RESULTS.GRANTED
          ) {
            callback && callback();
          } else {
            Alert.alert('Error', 'One of the permissions was not granted');
          }
        });
      } else if (
        statuses[CAMERA] === RESULTS.DENIED ||
        statuses[AUDIO] === RESULTS.DENIED
      ) {
        request(statuses[CAMERA] === RESULTS.DENIED ? CAMERA : AUDIO).then(
          (result) => {
            if (result === RESULTS.GRANTED) {
              callback && callback();
            } else {
              Alert.alert('Error', 'Permission not granted');
            }
          },
        );
      } else if (
        statuses[CAMERA] === RESULTS.GRANTED ||
        statuses[AUDIO] === RESULTS.GRANTED
      ) {
        callback && callback();
      }
    }
  });
};

ثم استدعِ هذه الدالة عند تشغيل الشاشة لأول مرة باستخدام useEffect:

useEffect(() => {
  _checkPermissions();
}, []);

إنشاء نموذج الانضمام إلى الغرفة

الخطوة التالية هي بناء نموذج يحتوي على حقلين: اسم المستخدم واسم الغرفة. عند الضغط على زر الاتصال، يرسل التطبيق اسم المستخدم إلى الخادم للحصول على JWT Token ثم ينتقل إلى شاشة المكالمة:

return (
  <KeyboardAvoidingView
    behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
    style={styles.container}>
    <ScrollView contentContainerStyle={styles.container}>
      <View style={styles.form}>
        <View style={styles.formGroup}>
          <Text style={styles.text}>User Name</Text>
          <TextInput
            style={styles.textInput}
            autoCapitalize="none"
            value={props.userName}
            onChangeText={(text) => setProps({...props, userName: text})}
          />
        </View>

        <View style={styles.formGroup}>
          <Text style={styles.text}>Room Name</Text>
          <TextInput
            style={styles.textInput}
            autoCapitalize="none"
            value={props.roomName}
            onChangeText={(text) => setProps({...props, roomName: text})}
          />
        </View>

        <View style={styles.formGroup}>
          <TouchableOpacity
            disabled={false}
            style={styles.button}
            onPress={() => {
              _checkPermissions(() => {
                fetch(`https://ae7a722dc260.ngrok.io/getToken?userName=${props.userName}`)
                  .then((response) => {
                    if (response.ok) {
                      response.text().then((jwt) => {
                        setProps({...props, token: jwt});
                        navigation.navigate('Video Call');
                        return true;
                      });
                    } else {
                      response.text().then((error) => {
                        Alert.alert(error);
                      });
                    }
                  })
                  .catch((error) => {
                    console.log('error', error);
                    Alert.alert('API not available');
                  });
              });
            }}>
            <Text style={styles.buttonText}>Connect to Video Call</Text>
          </TouchableOpacity>
        </View>
      </View>
    </ScrollView>
  </KeyboardAvoidingView>
);

شاشة تسجيل الغرفة في تطبيق React Native قبل بدء مكالمة الفيديو

بناء شاشة مكالمة الفيديو

في هذه الشاشة سنعرض بث الكاميرا المحلية، ومقاطع الفيديو الخاصة بالمشاركين الآخرين داخل الغرفة، بالإضافة إلى أزرار التحكم الأساسية.

تهيئة المرجع والحالة

const VideoCallScreen = ({navigation}) => {
  const twilioVideo = useRef(null);
  const {props, setProps} = useContext(AppContext);

بدء الاتصال بالغرفة

بمجرد فتح الشاشة، نستخدم الدالة connect() لربط المستخدم بالغرفة عبر الاسم والرمز:

useEffect(() => {
  twilioVideo.current.connect({
    roomName: props.roomName,
    accessToken: props.token,
  });

  setProps({...props, status: 'connecting'});

  return () => {
    _onEndButtonPress();
  };
}, []);

عرض فيديو المشاركين

بعد نجاح الاتصال، يمكن عرض فيديو الطرف الآخر بشكل شرطي عند توفر مسارات الفيديو:

{(props.status === 'connected' || props.status === 'connecting') && (
  <View style={styles.callWrapper}>
    {props.status === 'connected' && (
      <View style={styles.grid}>
        {Array.from(props.videoTracks, ([trackSid, trackIdentifier]) => (
          <TwilioVideoParticipantView
            style={styles.remoteVideo}
            key={trackSid}
            trackIdentifier={trackIdentifier}
          />
        ))}
      </View>
    )}
  </View>
)}

إضافة أزرار التحكم أثناء المكالمة

لتحسين تجربة المستخدم، نضيف ثلاث وظائف رئيسية: إنهاء المكالمة، كتم الصوت، وقلب الكاميرا بين الأمامية والخلفية.

const _onEndButtonPress = () => {
  twilioVideo.current.disconnect();
  setProps(initialState);
};

const _onMuteButtonPress = () => {
  twilioVideo.current
    .setLocalAudioEnabled(!props.isAudioEnabled)
    .then((isEnabled) =>
      setProps({...props, isAudioEnabled: isEnabled}),
    );
};

const _onFlipButtonPress = () => {
  twilioVideo.current.flipCamera();
};

هذه الوظائف تعتمد على الدوال الجاهزة:

  • disconnect() لإنهاء الجلسة.
  • setLocalAudioEnabled() لتفعيل أو تعطيل الميكروفون.
  • flipCamera() للتبديل بين الكاميرات.

أما الواجهة الخاصة بالأزرار فتكون كالتالي:

<View style={styles.optionsContainer}>
  <TouchableOpacity style={styles.button} onPress={_onEndButtonPress}>
    <Text style={styles.buttonText}>End</Text>
  </TouchableOpacity>

  <TouchableOpacity style={styles.button} onPress={_onMuteButtonPress}>
    <Text style={styles.buttonText}>
      {props.isAudioEnabled ? 'Mute' : 'Unmute'}
    </Text>
  </TouchableOpacity>

  <TouchableOpacity style={styles.button} onPress={_onFlipButtonPress}>
    <Text style={styles.buttonText}>Flip</Text>
  </TouchableOpacity>
</View>

إضافة مكوّن TwilioVideo ومراقبة الأحداث

هذا المكوّن هو المسؤول عن إدارة أحداث الغرفة والمشاركين وتحديث حالة التطبيق:

<TwilioVideo
  ref={twilioVideo}
  onRoomDidConnect={() => {
    setProps({...props, status: 'connected'});
  }}
  onRoomDidDisconnect={() => {
    setProps({...props, status: 'disconnected'});
    navigation.goBack();
  }}
  onRoomDidFailToConnect={(error) => {
    Alert.alert('Error', error.error);
    setProps({...props, status: 'disconnected'});
    navigation.goBack();
  }}
  onParticipantAddedVideoTrack={({participant, track}) => {
    if (track.enabled) {
      setProps({
        ...props,
        videoTracks: new Map([
          ...props.videoTracks,
          [
            track.trackSid,
            {
              participantSid: participant.sid,
              videoTrackSid: track.trackSid,
            },
          ],
        ]),
      });
    }
  }}
  onParticipantRemovedVideoTrack={({track}) => {
    const videoTracks = props.videoTracks;
    videoTracks.delete(track.trackSid);
    setProps({...props, videoTracks});
  }}
/>

هذا التكامل يتيح لك:

  • تحديث حالة الاتصال عند نجاح الربط.
  • إعادة المستخدم للشاشة السابقة عند انتهاء الجلسة.
  • التعامل مع أخطاء الاتصال بطريقة واضحة.
  • إضافة وإزالة مسارات الفيديو ديناميكياً عند دخول المشاركين أو خروجهم.

مكالمة فيديو بين مشاركين داخل تطبيق React Native باستخدام Twilio

نصائح تقنية لتحسين الجودة والاستقرار

إذا كنت تنوي تحويل هذا المثال إلى مشروع إنتاجي حقيقي، فهناك نقاط مهمة يجب مراعاتها:

  • استبدال رابط ngrok المؤقت بخادم دائم وآمن.
  • إضافة تحقق من صحة حقول userName وroomName قبل إرسالها.
  • تأمين الخادم ضد الطلبات العشوائية أو المفرطة.
  • تسجيل الأخطاء عبر نظام مراقبة مثل Sentry أو أي خدمة مشابهة.
  • تحسين تجربة المستخدم عبر شاشات انتظار ورسائل حالة واضحة.
  • إضافة دعم للمكالمات الجماعية إذا كان المنتج يحتاج لذلك.

الخلاصة التقنية

إضافة مكالمات الفيديو إلى تطبيق React Native باستخدام Twilio ليست معقدة كما تبدو، لكنها تتطلب فهماً جيداً لتكامل الواجهة الأمامية مع خادم يولّد رموز الوصول بشكل آمن. هذا النموذج يوفر أساساً عملياً قوياً يمكن البناء عليه لإطلاق ميزات أكثر تقدماً مثل الغرف متعددة المشاركين، تسجيل الجلسات، أو إدارة جودة الاتصال. من الناحية التقنية، يعد فصل التوكن عن التطبيق ووضعه في خادم مستقل خطوة أساسية لأي تطبيق احترافي قابل للتوسع وآمن في الوقت نفسه.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *