DOCUMENTATION TECHNIQUE

Migration ZegoCloud → Agora

Chat (Messagerie) + Appels Vidéo/Audio — Android & iOS
Application Wajd Care · Juin 2026 · Version 2.0

Table des matières

  1. Contexte de la migration
  2. Nouvelle architecture
  3. Endpoints API
  4. Connexion Agora Chat (Login)
  5. Flux complet du Chat
  6. Flux des appels Vidéo/Audio
  7. Modifications Backend Laravel
  8. Modifications Android
  9. Modifications iOS
  10. Identifiants Agora
  11. Résumé comparatif

1. Contexte de la migration

1.1 Problèmes avec ZegoCloud

1.2 Avantages d'Agora

2. Nouvelle architecture

2.1 Concepts clés

ConceptDescription
room_name Identifiant unique de la consultation, format : "consulting" + consulting.id.
Exemple : consulting1380.
Utilisé comme nom de groupe Agora Chat et comme channel_name pour les appels RTC.
Généré par Laravel lors de la création de la consultation.
group_id Identifiant du groupe Agora Chat, auto-généré par Agora.
Exemple : 315697874927617.
Retourné par l'API REST Agora lors de la création du groupe.
Sauvegardé dans consultings.group_id par Laravel.
username Identifiant utilisateur Agora Chat, dérivé de l'ID utilisateur.
Règle : minuscules + préfixe u si commence par un chiffre.
Exemple : ID 346 → username u346

2.2 Flux — Consultation programmée (scheduled)

1
Patient réserveLe patient appelle storeConsultingScheduled(). Laravel crée la consultation avec doctor_uuid déjà assigné.
2
room_name généréLaravel définit room_name = "consulting" + consulting.id (ex: consulting1380).
3
createGroupAgora()Laravel enregistre les deux utilisateurs sur Agora Chat, puis crée le groupe. Agora retourne un group_id auto-généré (ex: 315697874927617).
4
group_id sauvegardéLe group_id est sauvegardé dans la base de données (consultings.group_id).
5
Mobile rejointLe mobile récupère le group_id via getConsulting() et ouvre le chat directement.

2.3 Flux — Consultation immédiate (immediate)

1
Patient crée la demandeAppel à store(). Pas de médecin assigné — doctor_uuid = null.
2
Médecin accepteLe médecin appelle acceptConsulting(). Laravel assigne doctor_uuid.
3
createGroupAgora()Appelé automatiquement dans acceptConsulting(). Le groupe est créé côté serveur.
4
Les deux utilisateurs rejoignentChacun récupère le group_id et rejoint le même groupe.
Important
Le mobile ne crée JAMAIS de groupe. L'endpoint saveGroupId depuis le mobile n'est plus utilisé pour la création. Le backend Laravel gère tout automatiquement.

3. Endpoints API

3.1 POST agora/chat-token

Authentification : Bearer Token (Sanctum)
Entrées : Aucune — l'utilisateur est déduit de l'authentification.
Utilisation : Appelé par le mobile avant d'ouvrir l'écran de chat. Le token retourné est passé à AgoraChatHelper.login().

Réponse :

{
  "status": true,
  "message": "success",
  "data": {
    "token": "007eJxTYOhdrv0y6GdG0CVlX6WC...",
    "username": "u346"
  },
  "errors": []
}
ChampDescription
data.tokenToken Agora Chat généré par Laravel. Durée : 1 heure. Passé à ChatClient.loginWithAgoraToken(username, token).
data.usernameLe nom d'utilisateur Agora (sanitized). L'ID 346 devient "u346" car Agora interdit les usernames commençant par un chiffre.

3.2 POST agora/rtc-token

Authentification : Bearer Token (Sanctum)

Paramètres d'entrée :

ParamètreTypeDescription
channel_name String (requis) Le nom du canal RTC. Correspond à consulting.room_name.
Exemple : "consulting1380"
Les deux utilisateurs (patient + médecin) doivent utiliser le même channel_name pour se voir/entendre.
Android : consulting.getRoom_name()
iOS : consulting.roomName
uid int (optionnel) Identifiant unique dans le canal. Valeur recommandée : 0 (Agora assigne automatiquement).
Le même uid doit être utilisé dans rtcEngine.joinChannel(token, channelName, uid, options).

Réponse :

{
  "status": true,
  "message": "success",
  "data": {
    "token": "007eJxTYMg66/4v/P6ZxBwPRd3I...",
    "channel_name": "consulting1380",
    "uid": 0,
    "app_id": "cdc6c95ed75d45cda0a45ee86a4c6c64"
  },
  "errors": []
}
ChampDescription
data.tokenToken RTC. Durée : 1 heure. Passé à rtcEngine.joinChannel(token, channelName, uid, options).
data.channel_nameEcho du channel_name envoyé. = room_name de la consultation.
data.uidUID (0 = auto-assigné par Agora lors du joinChannel).
data.app_idL'App ID Agora, utile si le mobile ne le stocke pas en dur.
Note
Le channel_name est toujours consulting.room_name (ex: "consulting1380"). C'est le même pour le patient et le médecin. Chaque consultation a un canal unique.

4. Connexion Agora Chat — AgoraChatHelper

La classe AgoraChatHelper gère toute la logique de connexion à Agora Chat. Elle remplace ZIMKit.connectUser().

4.1 Méthode principale : login(userId, token, callback)

1
Sanitize le usernamesanitizeUsername("346")"u346". Minuscules, préfixe u si commence par un chiffre. Max 64 caractères.
2
Vérifie l'état actuelSi un autre utilisateur est connecté (ex: médecin sur le même appareil) → logout d'abord, puis login.
3
Si même utilisateur déjà connecté ET isConnected() = true → Retourne onSuccess() immédiatement. Pas besoin de re-login.
4
Appelle loginWithAgoraToken()Connexion réelle au serveur Agora. Toujours effectuée si pas déjà connecté.
5
Gestion des erreurs
  • Code 200 = déjà connecté → traité comme succès
  • Code 218 = autre utilisateur encore connecté → force logout + retry
  • Autres codes = échec réel

Code complet :

// 1. Sanitize
String username = sanitizeUsername(userId); // "346" → "u346"

// 2. Autre utilisateur connecté ? → logout d'abord
if (isLoggedIn && currentUser != username) {
    ChatClient.logout() → then doLogin(username, token);
    return;
}

// 3. Même utilisateur ET connecté ? → succès immédiat
if (isLoggedIn && currentUser == username && isConnected()) {
    callback.onSuccess();
    return;
}

// 4. Login réel
ChatClient.loginWithAgoraToken(username, token, callback);

// 5. Si erreur 218 → force logout + retry une fois

4.2 Fonction sanitizeUsername()

Critique
Cette fonction DOIT être identique côté Android, iOS, et Laravel. Une différence = l'utilisateur ne peut pas se connecter.
Entrée (user ID)Sortie (username Agora)Règle appliquée
346u346Préfixe u (commence par chiffre)
330u330Préfixe u
abc-123abc-123Déjà valide
Dr. Ahmeddr._ahmedMinuscules + caractères spéciaux → _

4.3 Quand le login est appelé

ÉcranQuandPourquoi
AgoraChatActivityonChatTokenReceived()Connexion au Chat pour envoyer/recevoir des messages
ConsultingAdapterN/A — suppriméAvant : ZIMKit.connectUser() dans startChatActivity(). Maintenant supprimé. Le login se fait dans AgoraChatActivity.
ConsultingDoctorAdapterN/A — suppriméAvant : ZIMKit.connectUser() + ZIMKit.createGroup() dans le bouton Accepter. Maintenant supprimé.

5. Flux complet du Chat — AgoraChatActivity

5.1 Ouverture de l'écran

Quand l'utilisateur (patient ou médecin) appuie sur le bouton "Entrer dans la session" :

1
onCreate()Lance deux appels API en parallèle :
  • presenter.getConsulting(consulting.getId()) — récupère les données fraîches (dont group_id)
  • presenter.getAgoraChatToken() — récupère le token Agora Chat
2
onChatTokenReceived()Le token arrive → appelle AgoraChatHelper.login() → connexion réelle → isLoggedIn = true → appelle tryOpenChat()
3
setConsulting()Les données de la consultation arrivent → extrait group_idisGroupIdReady = true → appelle tryOpenChat()
4
tryOpenChat()Vérifie les 3 conditions :
  • isLoggedIn == true (token reçu + login réussi)
  • isGroupIdReady == true (group_id récupéré du backend)
  • ChatClient.isConnected() == true (connexion réelle au serveur)
Si les 3 sont vraies → appelle showChatFragment().
Si isConnected() est false → retry après 500ms.
5
showChatFragment(groupId)Affiche le fragment UIKit Agora Chat.

5.2 setConsulting() — Callback du backend

Cette méthode est appelée automatiquement par le CallPresenter quand la réponse de getConsulting() arrive :

@Override
public void setConsulting(Consulting cons) {
    // cons contient les données fraîches de la consultation
    // y compris group_id créé par Laravel

    if (cons.getGroup_id() != null && !cons.getGroup_id().trim().isEmpty()) {
        // ✅ group_id existe → le sauvegarder localement
        this.groupId = cons.getGroup_id();
        isGroupIdReady = true;
        tryOpenChat(); // Tente d'ouvrir le chat
    } else {
        // ❌ Pas de group_id → le backend n'a pas encore créé le groupe
        // Cas possible : consultation immédiate, médecin pas encore assigné
        Toast.makeText(this, "المحادثة غير جاهزة", Toast.LENGTH_SHORT).show();
    }
}
Pourquoi récupérer le consulting à chaque ouverture ?
Le group_id peut avoir été créé entre le moment où la liste des consultations a été chargée et le moment où l'utilisateur ouvre le chat. Récupérer les données fraîches garantit que le group_id est à jour.

5.3 showChatFragment() — Affichage du UIKit

private void showChatFragment(String groupId) {
    EaseChatFragment fragment = new EaseChatFragment.Builder(
            groupId,                              // "315697874927617"
            EaseChatType.GROUP_CHAT               // Type : groupe (pas 1-to-1)
    )
    .useHeader(false)                         // On utilise notre propre toolbar
    .build();

    getSupportFragmentManager()
        .beginTransaction()
        .replace(R.id.fra_message, fragment)   // FrameLayout dans le layout XML
        .commit();
}
ParamètreDescription
groupIdL'ID du groupe Agora Chat (ex: "315697874927617"). Pas le room_name.
EaseChatType.GROUP_CHATIndique que c'est une conversation de groupe (pas un chat 1-to-1).
useHeader(false)Désactive le header intégré du UIKit car on a notre propre toolbar avec le nom + avatar + boutons appel.
R.id.fra_messageLe FrameLayout dans le layout XML où le fragment est inséré.

Le EaseChatFragment fournit automatiquement :

5.4 Layout XML

<!-- Votre toolbar personnalisée (nom, avatar, boutons appel) -->
<Toolbar android:id="@+id/chatToolbar" ... />

<!-- Le UIKit fragment remplit tout l'espace restant -->
<FrameLayout
    android:id="@+id/fra_message"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

<!-- Plus besoin de RecyclerView, EditText, ou bouton Send -->
<!-- Le EaseChatFragment fournit tout ça automatiquement -->

6. Flux des appels Vidéo/Audio

6.1 Démarrage d'un appel

1
PermissionsDemande caméra + microphone → si accordées, lit les extras de l'intent : room (channel_name), chat (0=audio, 1=vidéo).
2
Fetch RTC Tokenpresenter.getAgoraRtcToken(channelName, 0) → appel API Laravel → retourne le token.
3
onRtcTokenReceived()Token reçu → initialise le moteur RTC → configure audio/vidéo → rejoint le canal.
4
Foreground ServiceDémarre CallForegroundService avec notification "مكالمة جارية" pour garder l'audio actif en arrière-plan.
5
Remote User JoinQuand l'autre utilisateur rejoint, onUserJoined() affiche sa vidéo (mode vidéo) ou garde juste l'audio (mode audio).

6.2 Mode audio vs vidéo

Audio (chat=0)Vidéo (chat=1)
CaméraDésactivéeActivée
Preview localCachéAffiché (150×220dp)
Remote viewCaché (fond blanc)Plein écran
Boutons camera/switchCachésVisibles
Boutons mute/speakerVisiblesVisibles

7. Modifications Backend Laravel

7.1 Configuration .env

AGORA_APP_ID=cdc6c95ed75d45cda0a45ee86a4c6c64
AGORA_APP_CERTIFICATE=votre_certificat
AGORA_CHAT_ORG_NAME=41200031602
AGORA_CHAT_APP_NAME=200043726
AGORA_CHAT_DOMAIN=a41.chat.agora.io

7.2 AgoraChatService.php

MéthodeDescription
getAppToken()Token REST API. Caché 55 min.
getChatUserToken(userId)Token utilisateur pour le SDK mobile.
getRtcToken(channel, uid)Token pour les appels vidéo/audio.
registerUser(userId, nickname)Enregistre un utilisateur. 409 = déjà existant = OK.
createGroup(name, owner, members)Crée un groupe. ID auto-généré par Agora.
updateUserInfo(userId, name, avatar)Met à jour nom + avatar sur Agora.

7.3 Où createGroupAgora() est appelé

MéthodeContrôleurQuand
storeConsultingScheduled()ConsultingControllerAprès sauvegarde du room_name
addConsultingFromPack()ConsultingControllerAprès sauvegarde du room_name
addConsultingForSameDoctor()ConsultingControllerAprès sauvegarde du room_name
addConsultingForAnotherDoctor()ConsultingControllerAprès sauvegarde du room_name
acceptConsulting()DoctorControllerAprès assignation du doctor_uuid

7.4 Dépendance

composer require boogiefromzk/agora-token

7.5 Fichiers supprimés

FichierRemplacé par
app/Services/ZegoZimService.php SuppriméAgoraChatService.php

8. Modifications Android

8.1 Dépendances Gradle

Supprimer (ZegoCloud) SuppriméAjouter (Agora) Nouveau
im.zego:zim:x.x.xio.agora.rtc:chat-uikit:1.3.0
im.zego:express-video:x.x.xio.agora.rtc:full-sdk:4.3.0
ZIMKit dependency(UIKit inclut le Chat SDK)

8.2 Nouveaux fichiers

Fichier NouveauDescription
App.javaInitialise EaseUIKit + UserProfileProvider pour les avatars et noms
AgoraChatHelper.javaLogin Agora Chat avec gestion multi-utilisateur, retry, et logout automatique
AgoraUserCache.javaCache mémoire des profils (nom + avatar) pour le UserProfileProvider
AgoraChatActivity.javaÉcran de chat. Utilise EaseChatFragment pour l'interface complète
AgoraCallActivity.javaAppels vidéo/audio. Utilise Agora RTC SDK
CallForegroundService.javaService en premier plan pour garder l'audio actif en arrière-plan
AgoraChatToken.javaModèle réponse API (token + username)
AgoraRtcToken.javaModèle réponse API (token + channel_name + uid + app_id)
CallPresenter.javagetAgoraChatToken() et getAgoraRtcToken() sans progress dialog
CallView.javaonChatTokenReceived() et onRtcTokenReceived()

8.3 Fichiers modifiés

Fichier ModifiéChangement
ConsultingAdapter.javaSupprimer ZIMKit.connectUser() de startChatActivity(). Ouvrir AgoraChatActivity avec group_id
ConsultingDoctorAdapter.javaChangement majeur : Bouton Accepter → supprimer ZIMKit.createGroup() + saveGroupId(). Garder uniquement presenter.acceptConsulting()
ApiHelper.javaAjouter getAgoraChatToken() et getAgoraRtcToken()
ApiEndPoint.javaAjouter AGORA_CHAT_TOKEN et AGORA_RTC_TOKEN
AndroidManifest.xmlFileProvider, permissions foreground service, CallForegroundService

8.4 Fichiers supprimés

Supprimé SuppriméRemplacé par
ZimKitActivity.javaAgoraChatActivity.java
ZegoCallActivity.javaAgoraCallActivity.java
ChatService.javaAgoraChatHelper.java
activity_zim_kit.xmlactivity_agora_chat.xml

9. Modifications iOS

9.1 Dépendances

# Podfile
pod 'AgoraChat', '~> 1.3.0'
pod 'AgoraChat_iOS', '~> 1.3.0'    # UIKit
pod 'AgoraRtcEngine_iOS', '~> 4.3.0'

9.2 Changements principaux

Point critique
Le username Agora doit correspondre exactement à celui généré par Laravel. La fonction sanitizeUsername() convertit l'ID en minuscules et préfixe avec u si l'ID commence par un chiffre. Exemple : ID 346u346.

10. Identifiants Agora

ParamètreValeur
App IDcdc6c95ed75d45cda0a45ee86a4c6c64
App Key (Chat)41200031602#200043726
Consolehttps://console.agora.io
Sécurité
L'App Certificate ne doit PAS être stocké dans le code mobile. Il est uniquement dans le .env Laravel pour la génération de tokens côté serveur.

11. Résumé comparatif

ComposantAvant (ZegoCloud)Après (Agora)
Chat SDKZIMKitAgora Chat UIKit (EaseUIKit)
Appels vidéo/audioZegoExpress SDKAgora RTC SDK
Création groupeMobile (ZIMKit.createGroup)Backend Laravel (API REST Agora)
Group IDCustom ou auto (#AbWyGIH9S)Auto-généré par Agora (315697874927617)
saveGroupId (mobile)Appelé depuis Android/iOSSupprimé — Laravel sauvegarde directement
Auth tokensAppSign dans le code mobileTokens générés par Laravel (/api/agora/*)
Interface chatZIMKitMessageFragmentEaseChatFragment (EaseUIKit)
Profils utilisateursNon géréEaseUserProfileProvider + AgoraUserCache
Appels en arrière-planNon supportéCallForegroundService + WakeLock
room_nameUtilisé comme GroupId (échouait)Utilisé comme nom de groupe + channel RTC