

# Étape 5 :Publier une vidéo et s’y abonner
<a name="getting-started-pub-sub"></a>

Vous pouvez publier/vous abonner (en temps réel) à IVS avec :
+ Les [kits SDK de diffusion IVS](https://docs.aws.amazon.com//ivs/latest/LowLatencyUserGuide/getting-started-set-up-streaming.html#broadcast-sdk) natifs, qui prennent en charge WebRTC et RTMPS. Nous le recommandons, en particulier pour des scénarios de production. Consultez les détails ci-dessous pour [Web](getting-started-pub-sub-web.md), [Android](getting-started-pub-sub-android.md) et [iOS](getting-started-pub-sub-ios.md).
+ La console Amazon IVS : elle convient pour tester les flux. Reportez-vous à ci-dessous.
+ D’autres logiciels et encodeurs matériels de diffusion : vous pouvez utiliser tout encodeur prenant en charge les protocoles RTMP, RTMPS ou WHIP. Consultez [Ingestion de flux](rt-stream-ingest.md) pour plus d’informations.

## Console IVS
<a name="getting-started-pub-sub-console"></a>

1. Ouvrez la [console Amazon IVS](https://console.aws.amazon.com/ivs).

   (Vous pouvez également accéder à la console Amazon IVS via la [console de gestion AWS](https://console.aws.amazon.com/).)

1. Dans le panneau de navigation, sélectionnez **Scènes**. (Si le panneau de navigation est réduit, agrandissez-le en sélectionnant une icône en forme de hamburger.)

1. Sélectionnez la scène à laquelle vous souhaitez vous abonner ou publier pour accéder à la page de détails.

1. Pour vous abonner : si la scène compte un ou plusieurs diffuseurs de publication, vous pouvez vous y abonner en appuyant sur le bouton **S’abonner**, sous l’onglet **S’abonner**. (Les onglets se trouvent sous la section **Configuration générale**.)

1. Pour publier :

   1. Sélectionnez l’onglet **Publier**.

   1. Vous serez invité à autoriser la console IVS à accéder à votre caméra et à votre microphone ; **autorisez** ces autorisations.

   1. Au bas de l’onglet **Publier**, utilisez les listes déroulantes pour sélectionner les périphériques d’entrée pour le microphone et la caméra.

   1. Pour commencer à publier, sélectionnez **Commencer à publier**.

   1. Pour consulter le contenu que vous avez publié, retournez à l’onglet **S’abonner**.

   1. Pour arrêter la publication, accédez à l’onglet **Publier** et appuyez sur le bouton **Arrêter la publication** en bas de l’écran.

**Remarque** : s’abonner et publier consomment des ressources, et un tarif horaire sera appliqué pour le temps passé connecté à la scène. Pour en savoir plus, consultez la section [Diffusion en temps réel](https://aws.amazon.com/ivs/pricing/#Real-Time_Streaming) sur la page de tarification IVS.

# Publication et abonnement avec le SDK de diffusion Web IVS
<a name="getting-started-pub-sub-web"></a>

Cette section explique les étapes nécessaires à la publication et à l'abonnement à une étape à l'aide de votre application web.

## Créer un modèle de code HTML
<a name="getting-started-pub-sub-web-html"></a>

Commençons par créer le modèle de code HTML et importons la bibliothèque sous forme de balise de script :

```
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <!-- Import the SDK -->
  <script src="https://web-broadcast.live-video.net/1.33.0/amazon-ivs-web-broadcast.js"></script>
</head>

<body>

<!-- TODO - fill in with next sections -->
<script src="./app.js"></script>

</body>
</html>
```

## Accepter la saisie du jeton et ajouter des boutons Rejoindre/Quitter
<a name="getting-started-pub-sub-web-join"></a>

Ici, nous remplissons le corps avec nos contrôles d’entrée. Ceux-ci prennent comme entrée le jeton, et ils configurent les boutons **Rejoindre** et **Quitter**. Généralement, les applications demandent le jeton à partir de l’API de votre application, mais dans cet exemple, vous allez copier et coller le jeton dans l’entrée du jeton.

```
<h1>IVS Real-Time Streaming</h1>
<hr />

<label for="token">Token</label>
<input type="text" id="token" name="token" />
<button class="button" id="join-button">Join</button>
<button class="button" id="leave-button" style="display: none;">Leave</button>
<hr />
```

## Ajouter des éléments de conteneur multimédia
<a name="getting-started-pub-sub-web-media"></a>

Ces éléments hébergeront les médias pour nos participants locaux et distants. Nous ajoutons une balise de script pour charger la logique de notre application définie dans `app.js`.

```
<!-- Local Participant -->
<div id="local-media"></div>

<!-- Remote Participants -->
<div id="remote-media"></div>

<!-- Load Script -->
<script src="./app.js"></script>
```

Ceci termine la page HTML et vous devriez le voir lors du chargement du fichier `index.html` dans un navigateur :

![\[Afficher le streaming en temps réel dans un navigateur : configuration HTML terminée.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/RT_Browser_View.png)


## Crée le fichier app.js
<a name="getting-started-pub-sub-web-appjs"></a>

Passons à la définition du contenu de notre fichier `app.js`. Commencez par importer toutes les propriétés requises depuis le répertoire global du SDK :

```
const {
  Stage,
  LocalStageStream,
  SubscribeType,
  StageEvents,
  ConnectionState,
  StreamType
} = IVSBroadcastClient;
```

## Créer les variables d’application
<a name="getting-started-pub-sub-web-vars"></a>

Définissez des variables pour contenir les références aux éléments HTML de nos boutons **Rejoindre** et **Quitter** et pour stocker l’état de l’application :

```
let joinButton = document.getElementById("join-button");
let leaveButton = document.getElementById("leave-button");

// Stage management
let stage;
let joining = false;
let connected = false;
let localCamera;
let localMic;
let cameraStageStream;
let micStageStream;
```

## Créer joinStage 1 : définition de la fonction et validation de la saisie
<a name="getting-started-pub-sub-web-joinstage1"></a>

La fonction `joinStage` prend le jeton d’entrée, crée une connexion à la scène et commence à publier la vidéo et l’audio extraits de `getUserMedia`.

Pour commencer, nous définissons la fonction et validons l’état et la saisie du jeton. Nous développerons cette fonction dans les prochaines sections.

```
const joinStage = async () => {
  if (connected || joining) {
    return;
  }
  joining = true;

  const token = document.getElementById("token").value;

  if (!token) {
    window.alert("Please enter a participant token");
    joining = false;
    return;
  }

  // Fill in with the next sections
};
```

## Créer joinStage 2 : publier le contenu multimédia
<a name="getting-started-pub-sub-web-joinstage2"></a>

Voici les médias qui seront publiés vers la scène :

```
async function getCamera() {
  // Use Max Width and Height
  return navigator.mediaDevices.getUserMedia({
    video: true,
    audio: false
  });
}

async function getMic() {
  return navigator.mediaDevices.getUserMedia({
    video: false,
    audio: true
  });
}

// Retrieve the User Media currently set on the page
localCamera = await getCamera();
localMic = await getMic();

// Create StageStreams for Audio and Video
cameraStageStream = new LocalStageStream(localCamera.getVideoTracks()[0]);
micStageStream = new LocalStageStream(localMic.getAudioTracks()[0]);
```

## Créer joinStage 3 : définir la stratégie de la scène et créer la scène
<a name="getting-started-pub-sub-web-joinstage3"></a>

Cette stratégie de scène est au cœur de la logique de décision utilisée par le SDK pour décider du contenu à publier et des participants auxquels s’abonner. Pour plus d’informations sur l’objectif de la fonction, consultez la section [Stratégie](web-publish-subscribe.md#web-publish-subscribe-concepts-strategy).

Cette stratégie est simple. Après avoir rejoint la scène, publiez les flux que vous venez de récupérer et abonnez-vous au son et à la vidéo de chaque participant distant :

```
const strategy = {
  stageStreamsToPublish() {
    return [cameraStageStream, micStageStream];
  },
  shouldPublishParticipant() {
    return true;
  },
  shouldSubscribeToParticipant() {
    return SubscribeType.AUDIO_VIDEO;
  }
};

stage = new Stage(token, strategy);
```

## Créer joinStage 4 : gérer les événements de la scène et le rendu multimédia
<a name="getting-started-pub-sub-web-joinstage4"></a>

Les scènes génèrent de nombreux événements. Nous devrons écouter les événements `STAGE_PARTICIPANT_STREAMS_ADDED` et `STAGE_PARTICIPANT_LEFT` pour afficher et supprimer du contenu multimédia vers et depuis la page. Un ensemble plus complet d’événements est répertorié dans [Événements](web-publish-subscribe.md#web-publish-subscribe-concepts-events).

Notez que nous créons ici quatre fonctions d’assistance pour nous aider à gérer les éléments DOM nécessaires : `setupParticipant`, `teardownParticipant`, `createVideoEl` et `createContainer`.

```
stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {
  connected = state === ConnectionState.CONNECTED;

  if (connected) {
    joining = false;
    joinButton.style = "display: none";
    leaveButton.style = "display: inline-block";
  }
});

stage.on(
  StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED,
  (participant, streams) => {
    console.log("Participant Media Added: ", participant, streams);

    let streamsToDisplay = streams;

    if (participant.isLocal) {
      // Ensure to exclude local audio streams, otherwise echo will occur
      streamsToDisplay = streams.filter(
        (stream) => stream.streamType === StreamType.VIDEO
      );
    }

    const videoEl = setupParticipant(participant);
    streamsToDisplay.forEach((stream) =>
      videoEl.srcObject.addTrack(stream.mediaStreamTrack)
    );
  }
);

stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant) => {
  console.log("Participant Left: ", participant);
  teardownParticipant(participant);
});


// Helper functions for managing DOM

function setupParticipant({ isLocal, id }) {
  const groupId = isLocal ? "local-media" : "remote-media";
  const groupContainer = document.getElementById(groupId);

  const participantContainerId = isLocal ? "local" : id;
  const participantContainer = createContainer(participantContainerId);
  const videoEl = createVideoEl(participantContainerId);

  participantContainer.appendChild(videoEl);
  groupContainer.appendChild(participantContainer);

  return videoEl;
}

function teardownParticipant({ isLocal, id }) {
  const groupId = isLocal ? "local-media" : "remote-media";
  const groupContainer = document.getElementById(groupId);
  const participantContainerId = isLocal ? "local" : id;

  const participantDiv = document.getElementById(
    participantContainerId + "-container"
  );
  if (!participantDiv) {
    return;
  }
  groupContainer.removeChild(participantDiv);
}

function createVideoEl(id) {
  const videoEl = document.createElement("video");
  videoEl.id = id;
  videoEl.autoplay = true;
  videoEl.playsInline = true;
  videoEl.srcObject = new MediaStream();
  return videoEl;
}

function createContainer(id) {
  const participantContainer = document.createElement("div");
  participantContainer.classList = "participant-container";
  participantContainer.id = id + "-container";

  return participantContainer;
}
```

## Créer joinStage 5 : rejoindre la scène
<a name="getting-started-pub-sub-web-joinstage5"></a>

Complétons notre fonction `joinStage` en rejoignant enfin la scène \$1

```
try {
  await stage.join();
} catch (err) {
  joining = false;
  connected = false;
  console.error(err.message);
}
```

## Créer leaveStage
<a name="getting-started-pub-sub-web-leavestage"></a>

Définissez la fonction `leaveStage` qui sera invoquée par le bouton « Quitter ».

```
const leaveStage = async () => {
  stage.leave();

  joining = false;
  connected = false;
};
```

## Initialiser les gestionnaires d’entrées et d’événements
<a name="getting-started-pub-sub-web-handlers"></a>

Nous allons ajouter une dernière fonction à notre fichier `app.js`. Cette fonction est invoquée dès le chargement de la page et établit des gestionnaires d’événements pour rejoindre et quitter la scène.

```
const init = async () => {
  try {
    // Prevents issues on Safari/FF so devices are not blank
    await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
  } catch (e) {
    alert(
      "Problem retrieving media! Enable camera and microphone permissions."
    );
  }

  joinButton.addEventListener("click", () => {
    joinStage();
  });

  leaveButton.addEventListener("click", () => {
    leaveStage();
    joinButton.style = "display: inline-block";
    leaveButton.style = "display: none";
  });
};

init(); // call the function
```

## Exécutez l’application et fournissez un jeton
<a name="getting-started-pub-sub-run-app"></a>

À ce stade, vous pouvez partager la page Web localement ou avec d’autres personnes, [ouvrir la page](#getting-started-pub-sub-web-media), saisir un jeton de participant et rejoindre l’étape.

## Quelle est la prochaine étape ?
<a name="getting-started-pub-sub-next"></a>

Pour obtenir des exemples plus détaillés impliquant npm, React, etc., consultez le [SDK de diffusion IVS : guide pour le Web (Streaming en temps réel)](broadcast-web.md).

# Publication et abonnement avec le SDK de diffusion Android IVS
<a name="getting-started-pub-sub-android"></a>

Cette section explique les étapes nécessaires à la publication et à l'abonnement à une étape à l'aide de votre application Android.

## Créer les vues
<a name="getting-started-pub-sub-android-views"></a>

Nous commençons par créer une disposition simple pour notre application à l’aide du fichier `activity_main.xml` créé automatiquement. La disposition contient un `EditText` pour ajouter un jeton, un `Button` Rejoindre, un `TextView` pour afficher l’état de la scène, et une `CheckBox` pour commuter l’état de publication.

![\[Configurez la disposition de publication pour votre application Android.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_Android_1.png)


Voici le code XML qui sous-tend la vue :

```
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:keepScreenOn="true"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".BasicActivity">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/main_controls_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/cardview_dark_background"
            android:padding="12dp"
            app:layout_constraintTop_toTopOf="parent">

            <EditText
                android:id="@+id/main_token"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:autofillHints="@null"
                android:backgroundTint="@color/white"
                android:hint="@string/token"
                android:imeOptions="actionDone"
                android:inputType="text"
                android:textColor="@color/white"
                app:layout_constraintEnd_toStartOf="@id/main_join"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <Button
                android:id="@+id/main_join"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:backgroundTint="@color/black"
                android:text="@string/join"
                android:textAllCaps="true"
                android:textColor="@color/white"
                android:textSize="16sp"
                app:layout_constraintBottom_toBottomOf="@+id/main_token"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@id/main_token" />

            <TextView
                android:id="@+id/main_state"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/state"
                android:textColor="@color/white"
                android:textSize="18sp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/main_token" />

            <TextView
                android:id="@+id/main_publish_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/publish"
                android:textColor="@color/white"
                android:textSize="18sp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@id/main_publish_checkbox"
                app:layout_constraintTop_toBottomOf="@id/main_token" />

            <CheckBox
                android:id="@+id/main_publish_checkbox"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:buttonTint="@color/white"
                android:checked="true"
                app:layout_constraintBottom_toBottomOf="@id/main_publish_text"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="@id/main_publish_text" />

        </androidx.constraintlayout.widget.ConstraintLayout>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/main_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintTop_toBottomOf="@+id/main_controls_container"
            app:layout_constraintBottom_toBottomOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
<layout>
```

Nous avons référencé quelques ID de chaînes ici, nous allons donc créer notre fichier `strings.xml` entier maintenant :

```
<resources>
    <string name="app_name">BasicRealTime</string>
    <string name="join">Join</string>
    <string name="leave">Leave</string>
    <string name="token">Participant Token</string>
    <string name="publish">Publish</string>
    <string name="state">State: %1$s</string>
</resources>
```

Lions ces vues dans le XML à notre `MainActivity.kt` :

```
import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

private lateinit var checkboxPublish: CheckBox
private lateinit var recyclerView: RecyclerView
private lateinit var buttonJoin: Button
private lateinit var textViewState: TextView
private lateinit var editTextToken: EditText

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    checkboxPublish = findViewById(R.id.main_publish_checkbox)
    recyclerView = findViewById(R.id.main_recycler_view)
    buttonJoin = findViewById(R.id.main_join)
    textViewState = findViewById(R.id.main_state)
    editTextToken = findViewById(R.id.main_token)
}
```

Nous allons maintenant créer une vue d’élément pour notre `RecyclerView`. Pour ce faire, effectuez un clic droit sur votre répertoire `res/layout` et sélectionnez **New > Layout Resource File**. Nommez ce nouveau fichier `item_stage_participant.xml`.

![\[Créez une vue d’élément pour votre application Android RecyclerView.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_Android_2.png)


La disposition de cet élément est simple : elle contient une vue permettant de rendre le flux vidéo d’un participant et une liste d’étiquettes permettant d’afficher des informations sur le participant :

![\[Créez une vue d’élément pour votre application Android RecyclerView - labels.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_Android_3.png)


Voici le XML :

```
<?xml version="1.0" encoding="utf-8"?>
<com.amazonaws.ivs.realtime.basicrealtime.ParticipantItem xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/participant_preview_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:background="@android:color/darker_gray" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:background="#50000000"
        android:orientation="vertical"
        android:paddingLeft="4dp"
        android:paddingTop="2dp"
        android:paddingRight="4dp"
        android:paddingBottom="2dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <TextView
            android:id="@+id/participant_participant_id"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@android:color/white"
            android:textSize="16sp"
            tools:text="You (Disconnected)" />

        <TextView
            android:id="@+id/participant_publishing"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@android:color/white"
            android:textSize="16sp"
            tools:text="NOT_PUBLISHED" />

        <TextView
            android:id="@+id/participant_subscribed"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@android:color/white"
            android:textSize="16sp"
            tools:text="NOT_SUBSCRIBED" />

        <TextView
            android:id="@+id/participant_video_muted"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@android:color/white"
            android:textSize="16sp"
            tools:text="Video Muted: false" />

        <TextView
            android:id="@+id/participant_audio_muted"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@android:color/white"
            android:textSize="16sp"
            tools:text="Audio Muted: false" />

        <TextView
            android:id="@+id/participant_audio_level"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@android:color/white"
            android:textSize="16sp"
            tools:text="Audio Level: -100 dB" />

    </LinearLayout>

</com.amazonaws.ivs.realtime.basicrealtime.ParticipantItem>
```

Ce fichier XML renseigne une classe que nous n’avons pas encore créée : `ParticipantItem`. Étant donné que le XML inclut l’espace de nommage complet, veillez à mettre à jour ce fichier XML vers votre espace de nommage. Créons cette classe et configurons les vues, mais sinon, laissez-la vide pour le moment.

Créez une nouvelle classe Kotlin, `ParticipantItem` :

```
package com.amazonaws.ivs.realtime.basicrealtime

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.TextView
import kotlin.math.roundToInt

class ParticipantItem @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {

    private lateinit var previewContainer: FrameLayout
    private lateinit var textViewParticipantId: TextView
    private lateinit var textViewPublish: TextView
    private lateinit var textViewSubscribe: TextView
    private lateinit var textViewVideoMuted: TextView
    private lateinit var textViewAudioMuted: TextView
    private lateinit var textViewAudioLevel: TextView

    override fun onFinishInflate() {
        super.onFinishInflate()
        previewContainer = findViewById(R.id.participant_preview_container)
        textViewParticipantId = findViewById(R.id.participant_participant_id)
        textViewPublish = findViewById(R.id.participant_publishing)
        textViewSubscribe = findViewById(R.id.participant_subscribed)
        textViewVideoMuted = findViewById(R.id.participant_video_muted)
        textViewAudioMuted = findViewById(R.id.participant_audio_muted)
        textViewAudioLevel = findViewById(R.id.participant_audio_level)
    }
}
```

## Autorisations
<a name="getting-started-pub-sub-android-perms"></a>

Pour utiliser la caméra et le microphone, vous devez demander des autorisations à l’utilisateur. Pour cela, nous suivons un flux d’autorisations standard :

```
override fun onStart() {
    super.onStart()
    requestPermission()
}

private val requestPermissionLauncher =
    registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
        if (permissions[Manifest.permission.CAMERA] == true && permissions[Manifest.permission.RECORD_AUDIO] == true) {
            viewModel.permissionGranted() // we will add this later
        }
    }

private val permissions = listOf(
    Manifest.permission.CAMERA,
    Manifest.permission.RECORD_AUDIO,
)

private fun requestPermission() {
    when {
        this.hasPermissions(permissions) -> viewModel.permissionGranted() // we will add this later
        else -> requestPermissionLauncher.launch(permissions.toTypedArray())
    }
}

private fun Context.hasPermissions(permissions: List<String>): Boolean {
    return permissions.all {
        ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
    }
}
```

## État de l’application
<a name="getting-started-pub-sub-android-app-state"></a>

Notre application permet de suivre les participants au niveau local dans un fichier `MainViewModel.kt` et l’état sera communiqué au `MainActivity` en utilisant le [StateFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/) Kotlin.

Créez une classe Kotlin, `MainViewModel` :

```
package com.amazonaws.ivs.realtime.basicrealtime

import android.app.Application
import androidx.lifecycle.AndroidViewModel

class MainViewModel(application: Application) : AndroidViewModel(application), Stage.Strategy, StageRenderer {

}
```

Dans `MainActivity.kt`, nous gérons notre modèle de vue :

```
import androidx.activity.viewModels

private val viewModel: MainViewModel by viewModels()
```

Pour utiliser `AndroidViewModel` et ces extensions Kotlin `ViewModel`, vous devrez ajouter ce qui suit au fichier `build.gradle` de votre module :

```
implementation 'androidx.core:core-ktx:1.10.1'
implementation "androidx.activity:activity-ktx:1.7.2"
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.10.0'
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"

def lifecycle_version = "2.6.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
```

### RecyclerView Adapter
<a name="getting-started-pub-sub-android-app-state-recycler"></a>

Nous allons créer une sous-classe `RecyclerView.Adapter` pour suivre nos participants et mettre à jour notre `RecyclerView` sur les événements de la scène. Mais d’abord, nous avons besoin d’une classe qui représente un participant. Créez une classe Kotlin, `StageParticipant` :

```
package com.amazonaws.ivs.realtime.basicrealtime

import com.amazonaws.ivs.broadcast.Stage
import com.amazonaws.ivs.broadcast.StageStream

class StageParticipant(val isLocal: Boolean, var participantId: String?) {
    var publishState = Stage.PublishState.NOT_PUBLISHED
    var subscribeState = Stage.SubscribeState.NOT_SUBSCRIBED
    var streams = mutableListOf<StageStream>()

    val stableID: String
        get() {
            return if (isLocal) {
                "LocalUser"
            } else {
                requireNotNull(participantId)
            }
        }
}
```

Nous utiliserons cette classe dans classe `ParticipantAdapter` que nous allons créer ensuite. Nous commençons par définir la classe et créer une variable pour suivre les participants :

```
package com.amazonaws.ivs.realtime.basicrealtime

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView

class ParticipantAdapter : RecyclerView.Adapter<ParticipantAdapter.ViewHolder>() {

    private val participants = mutableListOf<StageParticipant>()
```

Nous devons également définir notre `RecyclerView.ViewHolder` avant d’implémenter le reste des surcharges :

```
class ViewHolder(val participantItem: ParticipantItem) : RecyclerView.ViewHolder(participantItem)
```

À partir de là, nous pouvons implémenter les surcharges `RecyclerView.Adapter` standard :

```
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val item = LayoutInflater.from(parent.context)
        .inflate(R.layout.item_stage_participant, parent, false) as ParticipantItem
    return ViewHolder(item)
}

override fun getItemCount(): Int {
    return participants.size
}

override fun getItemId(position: Int): Long =
    participants[position]
        .stableID
        .hashCode()
        .toLong()

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    return holder.participantItem.bind(participants[position])
}

override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
    val updates = payloads.filterIsInstance<StageParticipant>()
    if (updates.isNotEmpty()) {
        updates.forEach { holder.participantItem.bind(it) // implemented later }
    } else {
        super.onBindViewHolder(holder, position, payloads)
    }
}
```

Enfin, nous ajoutons de nouvelles méthodes que nous appellerons à partir de notre `MainViewModel` lorsque des modifications seront apportées aux participants. Ces méthodes sont des opérations CRUD standard sur l’adaptateur.

```
fun participantJoined(participant: StageParticipant) {
    participants.add(participant)
    notifyItemInserted(participants.size - 1)
}

fun participantLeft(participantId: String) {
    val index = participants.indexOfFirst { it.participantId == participantId }
    if (index != -1) {
        participants.removeAt(index)
        notifyItemRemoved(index)
    }
}

fun participantUpdated(participantId: String?, update: (participant: StageParticipant) -> Unit) {
    val index = participants.indexOfFirst { it.participantId == participantId }
    if (index != -1) {
        update(participants[index])
        notifyItemChanged(index, participants[index])
    }
}
```

En revenant au `MainViewModel`, nous devons créer et conserver une référence à cet adaptateur :

```
internal val participantAdapter = ParticipantAdapter()
```

## État de l’étape
<a name="getting-started-pub-sub-android-views-stage-state"></a>

Nous devons également suivre certains états de la scène au sein du `MainViewModel`. Définissons maintenant ces propriétés :

```
private val _connectionState = MutableStateFlow(Stage.ConnectionState.DISCONNECTED)
val connectionState = _connectionState.asStateFlow()

private var publishEnabled: Boolean = false
    set(value) {
        field = value
        // Because the strategy returns the value of `checkboxPublish.isChecked`, just call `refreshStrategy`.
        stage?.refreshStrategy()
    }

private var deviceDiscovery: DeviceDiscovery? = null
private var stage: Stage? = null
private var streams = mutableListOf<LocalStageStream>()
```

Pour voir votre propre aperçu avant de rejoindre une scène, nous créons immédiatement un participant local :

```
init {
    deviceDiscovery = DeviceDiscovery(application)

    // Create a local participant immediately to render our camera preview and microphone stats
    val localParticipant = StageParticipant(true, null)
    participantAdapter.participantJoined(localParticipant)
}
```

Nous voulons nous assurer de nettoyer ces ressources lorsque notre `ViewModel` est nettoyé. Nous surchargeons `onCleared()` immédiatement, afin de ne pas oublier de nettoyer ces ressources.

```
override fun onCleared() {
    stage?.release()
    deviceDiscovery?.release()
    deviceDiscovery = null
    super.onCleared()
}
```

Maintenant, nous peuplons notre propriété de `streams` locaux dès que les autorisations sont accordées, en implémentant la méthode `permissionsGranted` que nous avons appelée plus tôt :

```
internal fun permissionGranted() {
    val deviceDiscovery = deviceDiscovery ?: return
    streams.clear()
    val devices = deviceDiscovery.listLocalDevices()
    // Camera
    devices
        .filter { it.descriptor.type == Device.Descriptor.DeviceType.CAMERA }
        .maxByOrNull { it.descriptor.position == Device.Descriptor.Position.FRONT }
        ?.let { streams.add(ImageLocalStageStream(it)) }
    // Microphone
    devices
        .filter { it.descriptor.type == Device.Descriptor.DeviceType.MICROPHONE }
        .maxByOrNull { it.descriptor.isDefault }
        ?.let { streams.add(AudioLocalStageStream(it)) }

    stage?.refreshStrategy()

    // Update our local participant with these new streams
    participantAdapter.participantUpdated(null) {
        it.streams.clear()
        it.streams.addAll(streams)
    }
}
```

## Implémentation du SDK Scène
<a name="getting-started-pub-sub-android-stage-sdk"></a>

Trois [concepts](android-publish-subscribe.md#android-publish-subscribe-concepts) de base sous-tendent la fonctionnalité temps réel : scène, stratégie et moteur de rendu. L’objectif de la conception consiste à minimiser la quantité de logique côté client nécessaire à la création d’un produit fonctionnel.

### Stage.Strategy
<a name="getting-started-pub-sub-android-stage-sdk-strategy"></a>

L’implémentation de notre `Stage.Strategy` est simple :

```
override fun stageStreamsToPublishForParticipant(
    stage: Stage,
    participantInfo: ParticipantInfo
): MutableList<LocalStageStream> {
    // Return the camera and microphone to be published.
    // This is only called if `shouldPublishFromParticipant` returns true.
    return streams
}

override fun shouldPublishFromParticipant(stage: Stage, participantInfo: ParticipantInfo): Boolean {
    return publishEnabled
}

override fun shouldSubscribeToParticipant(stage: Stage, participantInfo: ParticipantInfo): Stage.SubscribeType {
    // Subscribe to both audio and video for all publishing participants.
    return Stage.SubscribeType.AUDIO_VIDEO
}
```

Pour résumer, nous publions sur la base de notre état `publishEnabled`, et si nous publions, nous publierons les flux que nous avons collectés précédemment. Enfin, pour cet exemple, nous nous abonnons toujours aux autres participants, pour recevoir à la fois leur audio et leur vidéo.

### StageRenderer
<a name="getting-started-pub-sub-android-stage-sdk-renderer"></a>

L’implémentation de `StageRenderer` est également assez simple, bien que compte tenu du nombre de fonctions, elle contienne un peu plus de code. L’approche générale de ce moteur de rendu consiste à mettre à jour notre `ParticipantAdapter` lorsque le SDK nous informe d’une modification apportée à un participant. Dans certains cas, nous traitons les participants locaux différemment, car nous avons décidé de les gérer nous-mêmes afin qu’ils puissent voir l’aperçu de leur caméra avant de rejoindre le groupe.

```
override fun onError(exception: BroadcastException) {
    Toast.makeText(getApplication(), "onError ${exception.localizedMessage}", Toast.LENGTH_LONG).show()
    Log.e("BasicRealTime", "onError $exception")
}

override fun onConnectionStateChanged(
    stage: Stage,
    connectionState: Stage.ConnectionState,
    exception: BroadcastException?
) {
    _connectionState.value = connectionState
}

override fun onParticipantJoined(stage: Stage, participantInfo: ParticipantInfo) {
    if (participantInfo.isLocal) {
        // If this is the local participant joining the stage, update the participant with a null ID because we
        // manually added that participant when setting up our preview
        participantAdapter.participantUpdated(null) {
            it.participantId = participantInfo.participantId
        }
    } else {
        // If they are not local, add them normally
        participantAdapter.participantJoined(
            StageParticipant(
                participantInfo.isLocal,
                participantInfo.participantId
            )
        )
    }
}

override fun onParticipantLeft(stage: Stage, participantInfo: ParticipantInfo) {
    if (participantInfo.isLocal) {
        // If this is the local participant leaving the stage, update the ID but keep it around because
        // we want to keep the camera preview active
        participantAdapter.participantUpdated(participantInfo.participantId) {
            it.participantId = null
        }
    } else {
        // If they are not local, have them leave normally
        participantAdapter.participantLeft(participantInfo.participantId)
    }
}

override fun onParticipantPublishStateChanged(
    stage: Stage,
    participantInfo: ParticipantInfo,
    publishState: Stage.PublishState
) {
    // Update the publishing state of this participant
    participantAdapter.participantUpdated(participantInfo.participantId) {
        it.publishState = publishState
    }
}

override fun onParticipantSubscribeStateChanged(
    stage: Stage,
    participantInfo: ParticipantInfo,
    subscribeState: Stage.SubscribeState
) {
    // Update the subscribe state of this participant
    participantAdapter.participantUpdated(participantInfo.participantId) {
        it.subscribeState = subscribeState
    }
}

override fun onStreamsAdded(stage: Stage, participantInfo: ParticipantInfo, streams: MutableList<StageStream>) {
    // We don't want to take any action for the local participant because we track those streams locally
    if (participantInfo.isLocal) {
        return
    }
    // For remote participants, add these new streams to that participant's streams array.
    participantAdapter.participantUpdated(participantInfo.participantId) {
        it.streams.addAll(streams)
    }
}

override fun onStreamsRemoved(stage: Stage, participantInfo: ParticipantInfo, streams: MutableList<StageStream>) {
    // We don't want to take any action for the local participant because we track those streams locally
    if (participantInfo.isLocal) {
        return
    }
    // For remote participants, remove these streams from that participant's streams array.
    participantAdapter.participantUpdated(participantInfo.participantId) {
        it.streams.removeAll(streams)
    }
}

override fun onStreamsMutedChanged(
    stage: Stage,
    participantInfo: ParticipantInfo,
    streams: MutableList<StageStream>
) {
    // We don't want to take any action for the local participant because we track those streams locally
    if (participantInfo.isLocal) {
        return
    }
    // For remote participants, notify the adapter that the participant has been updated. There is no need to modify
    // the `streams` property on the `StageParticipant` because it is the same `StageStream` instance. Just
    // query the `isMuted` property again.
    participantAdapter.participantUpdated(participantInfo.participantId) {}
}
```

## Implémentation d’un RecyclerView LayoutManager personnalisé
<a name="getting-started-pub-sub-android-layout"></a>

La répartition des différents nombres de participants peut s’avérer complexe. Vous souhaitez qu’ils occupent l’intégralité du cadre de la vue parent, mais vous ne souhaitez pas gérer la configuration de chaque participant de manière indépendante. Pour vous faciliter la tâche, nous allons procéder à l’implémentation d’un `RecyclerView.LayoutManager`.

Créez une autre classe, `StageLayoutManager`, qui étend `GridLayoutManager`. Cette classe est conçue pour calculer la disposition de chaque participant en fonction du nombre de participants dans une disposition en ligne/colonne basée sur le flux. Chaque ligne a la même hauteur que les autres, mais les colonnes peuvent avoir des largeurs différentes par ligne. Voir le commentaire de code au-dessus de la variable `layouts` pour une description de la façon de personnaliser ce comportement.

```
package com.amazonaws.ivs.realtime.basicrealtime

import android.content.Context
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView

class StageLayoutManager(context: Context?) : GridLayoutManager(context, 6) {

    companion object {
        /**
         * This 2D array contains the description of how the grid of participants should be rendered
         * The index of the 1st dimension is the number of participants needed to active that configuration
         * Meaning if there is 1 participant, index 0 will be used. If there are 5 participants, index 4 will be used.
         *
         * The 2nd dimension is a description of the layout. The length of the array is the number of rows that
         * will exist, and then each number within that array is the number of columns in each row.
         *
         * See the code comments next to each index for concrete examples.
         *
         * This can be customized to fit any layout configuration needed.
         */
        val layouts: List<List<Int>> = listOf(
            // 1 participant
            listOf(1), // 1 row, full width
            // 2 participants
            listOf(1, 1), // 2 rows, all columns are full width
            // 3 participants
            listOf(1, 2), // 2 rows, first row's column is full width then 2nd row's columns are 1/2 width
            // 4 participants
            listOf(2, 2), // 2 rows, all columns are 1/2 width
            // 5 participants
            listOf(1, 2, 2), // 3 rows, first row's column is full width, 2nd and 3rd row's columns are 1/2 width
            // 6 participants
            listOf(2, 2, 2), // 3 rows, all column are 1/2 width
            // 7 participants
            listOf(2, 2, 3), // 3 rows, 1st and 2nd row's columns are 1/2 width, 3rd row's columns are 1/3rd width
            // 8 participants
            listOf(2, 3, 3),
            // 9 participants
            listOf(3, 3, 3),
            // 10 participants
            listOf(2, 3, 2, 3),
            // 11 participants
            listOf(2, 3, 3, 3),
            // 12 participants
            listOf(3, 3, 3, 3),
        )
    }

    init {
        spanSizeLookup = object : SpanSizeLookup() {
            override fun getSpanSize(position: Int): Int {
                if (itemCount <= 0) {
                    return 1
                }
                // Calculate the row we're in
                val config = layouts[itemCount - 1]
                var row = 0
                var curPosition = position
                while (curPosition - config[row] >= 0) {
                    curPosition -= config[row]
                    row++
                }
                // spanCount == max spans, config[row] = number of columns we want
                // So spanCount / config[row] would be something like 6 / 3 if we want 3 columns.
                // So this will take up 2 spans, with a max of 6 is 1/3rd of the view.
                return spanCount / config[row]
            }
        }
    }

    override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
        if (itemCount <= 0 || state?.isPreLayout == true) return

        val parentHeight = height
        val itemHeight = parentHeight / layouts[itemCount - 1].size // height divided by number of rows.

        // Set the height of each view based on how many rows exist for the current participant count.
        for (i in 0 until childCount) {
            val child = getChildAt(i) ?: continue
            val layoutParams = child.layoutParams as RecyclerView.LayoutParams
            if (layoutParams.height != itemHeight) {
                layoutParams.height = itemHeight
                child.layoutParams = layoutParams
            }
        }
        // After we set the height for all our views, call super.
        // This works because our RecyclerView can not scroll and all views are always visible with stable IDs.
        super.onLayoutChildren(recycler, state)
    }

    override fun canScrollVertically(): Boolean = false
    override fun canScrollHorizontally(): Boolean = false
}
```

De retour dans `MainActivity.kt`, nous devons définir l’adaptateur et le gestionnaire de disposition pour notre `RecyclerView` :

```
// In onCreate after setting recyclerView.
recyclerView.layoutManager = StageLayoutManager(this)
recyclerView.adapter = viewModel.participantAdapter
```

## Connexion des actions de l’interface utilisateur
<a name="getting-started-pub-sub-android-actions"></a>

Nous nous rapprochons ; il ne nous reste plus que quelques actions d’interface utilisateur à connecter.

Tout d’abord, nous avons notre `MainActivity` qui observe les changements de `StateFlow` depuis le `MainViewModel` :

```
// At the end of your onCreate method
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.CREATED) {
        viewModel.connectionState.collect { state ->
            buttonJoin.setText(if (state == ConnectionState.DISCONNECTED) R.string.join else R.string.leave)
            textViewState.text = getString(R.string.state, state.name)
        }
    }
}
```

Ensuite, nous ajoutons des auditeurs à notre bouton Rejoindre et à la case à cocher Publier :

```
buttonJoin.setOnClickListener {
    viewModel.joinStage(editTextToken.text.toString())
}
checkboxPublish.setOnCheckedChangeListener { _, isChecked ->
    viewModel.setPublishEnabled(isChecked)
}
```

Les deux fonctionnalités d’appel ci-dessus sont disponibles dans notre `MainViewModel`, que nous implémentons maintenant :

```
internal fun joinStage(token: String) {
    if (_connectionState.value != Stage.ConnectionState.DISCONNECTED) {
        // If we're already connected to a stage, leave it.
        stage?.leave()
    } else {
        if (token.isEmpty()) {
            Toast.makeText(getApplication(), "Empty Token", Toast.LENGTH_SHORT).show()
            return
        }
        try {
            // Destroy the old stage first before creating a new one.
            stage?.release()
            val stage = Stage(getApplication(), token, this)
            stage.addRenderer(this)
            stage.join()
            this.stage = stage
        } catch (e: BroadcastException) {
            Toast.makeText(getApplication(), "Failed to join stage ${e.localizedMessage}", Toast.LENGTH_LONG).show()
            e.printStackTrace()
        }
    }
}

internal fun setPublishEnabled(enabled: Boolean) {
    publishEnabled = enabled
}
```

## Affichage des participants
<a name="getting-started-pub-sub-android-participants"></a>

Enfin, nous devons rendre les données que nous recevons du SDK sur l’élément participant que nous avons créé précédemment. La logique du `RecyclerView` est déjà terminée, il ne nous reste plus qu’à implémenter l’API `bind` dans `ParticipantItem`.

Nous allons commencer par ajouter une fonction vide, puis la parcourir étape par étape :

```
fun bind(participant: StageParticipant) {

}
```

Nous allons d’abord gérer l’état simplifié, l’identifiant du participant, l’état de publication et l’état d’abonnement. Pour ceux-ci, nous mettons simplement à jour nos `TextViews` directement :

```
val participantId = if (participant.isLocal) {
    "You (${participant.participantId ?: "Disconnected"})"
} else {
    participant.participantId
}
textViewParticipantId.text = participantId
textViewPublish.text = participant.publishState.name
textViewSubscribe.text = participant.subscribeState.name
```

Ensuite, nous mettrons à jour les états de coupure de l’audio et de la vidéo. Pour obtenir l’état de coupure, nous devons trouver `ImageDevice` et `AudioDevice` à partir du tableau de flux. Pour optimiser les performances, nous mémorisons les derniers identifiants des appareils connectés.

```
// This belongs outside the `bind` API.
private var imageDeviceUrn: String? = null
private var audioDeviceUrn: String? = null

// This belongs inside the `bind` API.
val newImageStream = participant
    .streams
    .firstOrNull { it.device is ImageDevice }
textViewVideoMuted.text = if (newImageStream != null) {
    if (newImageStream.muted) "Video muted" else "Video not muted"
} else {
    "No video stream"
}

val newAudioStream = participant
    .streams
    .firstOrNull { it.device is AudioDevice }
textViewAudioMuted.text = if (newAudioStream != null) {
    if (newAudioStream.muted) "Audio muted" else "Audio not muted"
} else {
    "No audio stream"
}
```

Enfin, nous voulons afficher un aperçu du `imageDevice` :

```
if (newImageStream?.device?.descriptor?.urn != imageDeviceUrn) {
    // If the device has changed, remove all subviews from the preview container
    previewContainer.removeAllViews()
    (newImageStream?.device as? ImageDevice)?.let {
        val preview = it.getPreviewView(BroadcastConfiguration.AspectMode.FIT)
        previewContainer.addView(preview)
        preview.layoutParams = FrameLayout.LayoutParams(
            FrameLayout.LayoutParams.MATCH_PARENT,
            FrameLayout.LayoutParams.MATCH_PARENT
        )
    }
}
imageDeviceUrn = newImageStream?.device?.descriptor?.urn
```

Et nous affichons les statistiques audio du `audioDevice` :

```
if (newAudioStream?.device?.descriptor?.urn != audioDeviceUrn) {
    (newAudioStream?.device as? AudioDevice)?.let {
        it.setStatsCallback { _, rms ->
            textViewAudioLevel.text = "Audio Level: ${rms.roundToInt()} dB"
        }
    }
}
audioDeviceUrn = newAudioStream?.device?.descriptor?.urn
```

# Publication et abonnement avec le SDK de diffusion iOS IVS
<a name="getting-started-pub-sub-ios"></a>

Cette section explique les étapes nécessaires à la publication et à l'abonnement à une étape à l'aide de votre application iOS.

## Créer les vues
<a name="getting-started-pub-sub-ios-views"></a>

Nous commençons par utiliser le fichier `ViewController.swift` créé automatiquement pour importer `AmazonIVSBroadcast`, puis ajouter quelques `@IBOutlets` à lier :

```
import AmazonIVSBroadcast

class ViewController: UIViewController {

    @IBOutlet private var textFieldToken: UITextField!
    @IBOutlet private var buttonJoin: UIButton!
    @IBOutlet private var labelState: UILabel!
    @IBOutlet private var switchPublish: UISwitch!
    @IBOutlet private var collectionViewParticipants: UICollectionView!
```

Nous créons maintenant ces vues et les relions dans `Main.storyboard`. Voici la structure de vue que nous allons utiliser :

![\[Utilisez Main.storyboard pour créer une vue iOS.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_iOS_1.png)


Pour la configuration d’AutoLayout, nous devons personnaliser trois vues. La première vue est **Collection View Participants** (une `UICollectionView`). Liez **Leading**, **Trailing**, et **Bottom** à **Safe Area**. Liez également **Top** à **Controls Container**.

![\[Personnaliser la vue iOS Collection View Participants.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_iOS_2.png)


La deuxième vue est **Controls Container**. Liez **Leading**, **Trailing**, et **Top** à **Safe Area** :

![\[Personnalisez la vue iOS Controls Container.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_iOS_3.png)


La troisième et dernière vue est **Vertical Stack View**. Liez **Top**, **Leading**, **Trailing**, et **Bottom** à **Superview**. Pour le style, définissez l’espacement sur 8 au lieu de 0.

![\[Personnalisez la vue iOS Vertical Stack view.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_iOS_4.png)


Les **UIStackViews** s’occupent de gérer la disposition des vues restantes. Pour les trois **UIStackViews**, utilisez **Fill** pour **Alignment** et **Distribution**.

![\[Personnalisez les vues iOS restantes avec UIStackViews.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_iOS_5.png)


Enfin, relions ces vues à notre `ViewController`. De dessus, cartographiez les vues suivantes :
+ **Text Field Join** est relié à `textFieldToken`.
+ **Button Join** est relié à `buttonJoin`.
+ **Label State** est relié à `labelState`.
+ **Switch Publish** est relié à `switchPublish`.
+ **Collection View Participants** est relié à `collectionViewParticipants`.

Profitez également de cette période pour définir la `dataSource` de l’élément **Collection View Participants** sur le `ViewController` propriétaire :

![\[Définissez la dataSource de Collection View Participants pour l’application iOS.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_iOS_6.png)


Nous créons maintenant la sous-classe `UICollectionViewCell` dans laquelle afficher les participants. Commencez par créer un fichier **Cocoa Touch Class** :

![\[Créez un UICollectionViewCell pour afficher les participants iOS en temps réel.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_iOS_7.png)


Nommez-le `ParticipantUICollectionViewCell` et faites-en une sous-classe de `UICollectionViewCell` dans Swift. Nous recommençons dans le fichier Swift, en créant nos `@IBOutlets` à lier :

```
import AmazonIVSBroadcast

class ParticipantCollectionViewCell: UICollectionViewCell {

    @IBOutlet private var viewPreviewContainer: UIView!
    @IBOutlet private var labelParticipantId: UILabel!
    @IBOutlet private var labelSubscribeState: UILabel!
    @IBOutlet private var labelPublishState: UILabel!
    @IBOutlet private var labelVideoMuted: UILabel!
    @IBOutlet private var labelAudioMuted: UILabel!
    @IBOutlet private var labelAudioVolume: UILabel!
```

Dans le fichier XIB associé, créez cette hiérarchie de vues :

![\[Créez une hiérarchie de vues iOS dans le fichier XIB associé.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_iOS_8.png)


Pour AutoLayout, nous allons à nouveau modifier trois vues. La première vue est **View Preview Container**. Liez **Trailing**, **Leading**, **Top** et **Bottom** à **Participant Collection View Cell**.

![\[Personnalisez la vue iOS View Preview Container.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_iOS_9.png)


La deuxième vue est **View**. Liez **Leading** et **Top** à **Participant Collection View Cell** et définissez la valeur sur 4.

![\[Personnalisez la vue iOS View.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_iOS_10.png)


La troisième vue est **Stack View**. Liez **Trailing**, **Leading**, **Top** et **Bottom** à **Superview** et définissez la valeur sur 4.

![\[Personnalisez la vue iOS Stack View.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_iOS_11.png)


## Autorisations et minuteur d’inactivité
<a name="getting-started-pub-sub-ios-perms"></a>

De retour à notre `ViewController`, nous allons désactiver le minuteur d’inactivité du système pour empêcher l’appareil de se mettre en veille pendant l’utilisation de notre application :

```
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    // Prevent the screen from turning off during a call.
    UIApplication.shared.isIdleTimerDisabled = true
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    UIApplication.shared.isIdleTimerDisabled = false
}
```

Ensuite, nous demandons les autorisations de caméra et de microphone auprès du système :

```
private func checkPermissions() {
    checkOrGetPermission(for: .video) { [weak self] granted in
        guard granted else {
            print("Video permission denied")
            return
        }
        self?.checkOrGetPermission(for: .audio) { [weak self] granted in
            guard granted else {
                print("Audio permission denied")
                return
            }
            self?.setupLocalUser() // we will cover this later
        }
    }
}

private func checkOrGetPermission(for mediaType: AVMediaType, _ result: @escaping (Bool) -> Void) {
    func mainThreadResult(_ success: Bool) {
        DispatchQueue.main.async {
            result(success)
        }
    }
    switch AVCaptureDevice.authorizationStatus(for: mediaType) {
    case .authorized: mainThreadResult(true)
    case .notDetermined:
        AVCaptureDevice.requestAccess(for: mediaType) { granted in
            mainThreadResult(granted)
        }
    case .denied, .restricted: mainThreadResult(false)
    @unknown default: mainThreadResult(false)
    }
}
```

## État de l’application
<a name="getting-started-pub-sub-ios-app-state"></a>

Nous devons configurer notre `collectionViewParticipants` avec le fichier de disposition que nous avons créé précédemment :

```
override func viewDidLoad() {
    super.viewDidLoad()
    // We render everything to exactly the frame, so don't allow scrolling.
    collectionViewParticipants.isScrollEnabled = false
    collectionViewParticipants.register(UINib(nibName: "ParticipantCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "ParticipantCollectionViewCell")
}
```

Pour représenter chaque participant, nous créons une structure simple appelée `StageParticipant`. Cela peut être inclus dans le fichier `ViewController.swift`, ou un nouveau fichier peut être créé.

```
import Foundation
import AmazonIVSBroadcast

struct StageParticipant {
    let isLocal: Bool
    var participantId: String?
    var publishState: IVSParticipantPublishState = .notPublished
    var subscribeState: IVSParticipantSubscribeState = .notSubscribed
    var streams: [IVSStageStream] = []

    init(isLocal: Bool, participantId: String?) {
        self.isLocal = isLocal
        self.participantId = participantId
    }
}
```

Pour suivre ces participants, nous en conservons une liste en tant que propriété privée dans notre `ViewController` :

```
private var participants = [StageParticipant]()
```

Cette propriété sera utilisée pour alimenter notre `UICollectionViewDataSource` qui était lié depuis le storyboard plus tôt :

```
extension ViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return participants.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ParticipantCollectionViewCell", for: indexPath) as? ParticipantCollectionViewCell {
            cell.set(participant: participants[indexPath.row])
            return cell
        } else {
            fatalError("Couldn't load custom cell type 'ParticipantCollectionViewCell'")
        }
    }

}
```

Pour voir votre propre aperçu avant de rejoindre une scène, nous créons immédiatement un participant local :

```
override func viewDidLoad() {
    /* existing UICollectionView code */
    participants.append(StageParticipant(isLocal: true, participantId: nil))
}
```

Cela se traduit par le rendu d’une cellule de participant immédiatement après l’exécution de l’application, représentant le participant local.

Les utilisateurs veulent pouvoir se voir eux-mêmes avant de rejoindre une scène. Nous allons donc implémenter la méthode `setupLocalUser()` qui est appelée précédemment par le code de gestion des autorisations. Nous stockons la référence de la caméra et du microphone en tant qu’objets `IVSLocalStageStream`.

```
private var streams = [IVSLocalStageStream]()
private let deviceDiscovery = IVSDeviceDiscovery()

private func setupLocalUser() {
    // Gather our camera and microphone once permissions have been granted
    let devices = deviceDiscovery.listLocalDevices()
    streams.removeAll()
    if let camera = devices.compactMap({ $0 as? IVSCamera }).first {
        streams.append(IVSLocalStageStream(device: camera))
        // Use a front camera if available.
        if let frontSource = camera.listAvailableInputSources().first(where: { $0.position == .front }) {
            camera.setPreferredInputSource(frontSource)
        }
    }
    if let mic = devices.compactMap({ $0 as? IVSMicrophone }).first {
        streams.append(IVSLocalStageStream(device: mic))
    }
    participants[0].streams = streams
    participantsChanged(index: 0, changeType: .updated)
}
```

Ici, nous avons trouvé la caméra et le microphone de l’appareil via le SDK et les avons stockés dans notre objet local `streams`, puis nous avons assigné le tableau de `streams` du premier participant (le participant local que nous avons créé plus tôt) à notre `streams`. Enfin, nous appelons `participantsChanged` avec un `index` défini sur 0 et un `changeType` défini sur `updated`. Cette fonction est une fonction d’aide à la mise à jour de notre `UICollectionView` avec de belles animations. Voici à quoi cela ressemble :

```
private func participantsChanged(index: Int, changeType: ChangeType) {
    switch changeType {
    case .joined:
        collectionViewParticipants?.insertItems(at: [IndexPath(item: index, section: 0)])
    case .updated:
        // Instead of doing reloadItems, just grab the cell and update it ourselves. It saves a create/destroy of a cell
        // and more importantly fixes some UI flicker. We disable scrolling so the index path per cell
        // never changes.
        if let cell = collectionViewParticipants?.cellForItem(at: IndexPath(item: index, section: 0)) as? ParticipantCollectionViewCell {
            cell.set(participant: participants[index])
        }
    case .left:
        collectionViewParticipants?.deleteItems(at: [IndexPath(item: index, section: 0)])
    }
}
```

Ne vous préoccupez pas encore de `cell.set` ; nous y reviendrons plus tard, mais c’est là que nous afficherons le contenu de la cellule en fonction du participant.

Le `ChangeType` est une simple énumération :

```
enum ChangeType {
    case joined, updated, left
}
```

Enfin, nous voulons savoir si la scène est connectée. Nous utilisons un simple `bool` pour suivre cela, qui mettra automatiquement à jour notre interface utilisateur lorsqu’elle sera elle-même mise à jour.

```
private var connectingOrConnected = false {
    didSet {
        buttonJoin.setTitle(connectingOrConnected ? "Leave" : "Join", for: .normal)
        buttonJoin.tintColor = connectingOrConnected ? .systemRed : .systemBlue
    }
}
```

## Implémenter le SDK Scène
<a name="getting-started-pub-sub-ios-stage-sdk"></a>

Trois [concepts](ios-publish-subscribe.md#ios-publish-subscribe-concepts) de base sous-tendent la fonctionnalité temps réel : scène, stratégie et moteur de rendu. L’objectif de la conception consiste à minimiser la quantité de logique côté client nécessaire à la création d’un produit fonctionnel.

### IVSStageStrategy
<a name="getting-started-pub-sub-ios-stage-sdk-strategy"></a>

L’implémentation de notre `IVSStageStrategy` est simple :

```
extension ViewController: IVSStageStrategy {
    func stage(_ stage: IVSStage, streamsToPublishForParticipant participant: IVSParticipantInfo) -> [IVSLocalStageStream] {
        // Return the camera and microphone to be published.
        // This is only called if `shouldPublishParticipant` returns true.
        return streams
    }

    func stage(_ stage: IVSStage, shouldPublishParticipant participant: IVSParticipantInfo) -> Bool {
        // Our publish status is based directly on the UISwitch view
        return switchPublish.isOn
    }

    func stage(_ stage: IVSStage, shouldSubscribeToParticipant participant: IVSParticipantInfo) -> IVSStageSubscribeType {
        // Subscribe to both audio and video for all publishing participants.
        return .audioVideo
    }
}
```

En résumé, nous ne publions que si l’interrupteur de publication est en position « activé », et si nous le faisons, seuls les flux que nous avons collectés précédemment sont publiés. Enfin, pour cet exemple, nous nous abonnons toujours aux autres participants, pour recevoir à la fois leur audio et leur vidéo.

### IVSStageRenderer
<a name="getting-started-pub-sub-ios-stage-sdk-renderer"></a>

L’implémentation de `IVSStageRenderer` est également assez simple, bien que compte tenu du nombre de fonctions, elle contienne un peu plus de code. L’approche générale de ce moteur de rendu consiste à mettre à jour notre tableau `participants` lorsque le SDK nous informe d’une modification apportée à un participant. Dans certains cas, nous traitons les participants locaux différemment, car nous avons décidé de les gérer nous-mêmes afin qu’ils puissent voir l’aperçu de leur caméra avant de rejoindre le groupe.

```
extension ViewController: IVSStageRenderer {

    func stage(_ stage: IVSStage, didChange connectionState: IVSStageConnectionState, withError error: Error?) {
        labelState.text = connectionState.text
        connectingOrConnected = connectionState != .disconnected
    }

    func stage(_ stage: IVSStage, participantDidJoin participant: IVSParticipantInfo) {
        if participant.isLocal {
            // If this is the local participant joining the Stage, update the first participant in our array because we
            // manually added that participant when setting up our preview
            participants[0].participantId = participant.participantId
            participantsChanged(index: 0, changeType: .updated)
        } else {
            // If they are not local, add them to the array as a newly joined participant.
            participants.append(StageParticipant(isLocal: false, participantId: participant.participantId))
            participantsChanged(index: (participants.count - 1), changeType: .joined)
        }
    }

    func stage(_ stage: IVSStage, participantDidLeave participant: IVSParticipantInfo) {
        if participant.isLocal {
            // If this is the local participant leaving the Stage, update the first participant in our array because
            // we want to keep the camera preview active
            participants[0].participantId = nil
            participantsChanged(index: 0, changeType: .updated)
        } else {
            // If they are not local, find their index and remove them from the array.
            if let index = participants.firstIndex(where: { $0.participantId == participant.participantId }) {
                participants.remove(at: index)
                participantsChanged(index: index, changeType: .left)
            }
        }
    }

    func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChange publishState: IVSParticipantPublishState) {
        // Update the publishing state of this participant
        mutatingParticipant(participant.participantId) { data in
            data.publishState = publishState
        }
    }

    func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChange subscribeState: IVSParticipantSubscribeState) {
        // Update the subscribe state of this participant
        mutatingParticipant(participant.participantId) { data in
            data.subscribeState = subscribeState
        }
    }

    func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChangeMutedStreams streams: [IVSStageStream]) {
        // We don't want to take any action for the local participant because we track those streams locally
        if participant.isLocal { return }
        // For remote participants, notify the UICollectionView that they have updated. There is no need to modify
        // the `streams` property on the `StageParticipant` because it is the same `IVSStageStream` instance. Just
        // query the `isMuted` property again.
        if let index = participants.firstIndex(where: { $0.participantId == participant.participantId }) {
            participantsChanged(index: index, changeType: .updated)
        }
    }

    func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didAdd streams: [IVSStageStream]) {
        // We don't want to take any action for the local participant because we track those streams locally
        if participant.isLocal { return }
        // For remote participants, add these new streams to that participant's streams array.
        mutatingParticipant(participant.participantId) { data in
            data.streams.append(contentsOf: streams)
        }
    }

    func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didRemove streams: [IVSStageStream]) {
        // We don't want to take any action for the local participant because we track those streams locally
        if participant.isLocal { return }
        // For remote participants, remove these streams from that participant's streams array.
        mutatingParticipant(participant.participantId) { data in
            let oldUrns = streams.map { $0.device.descriptor().urn }
            data.streams.removeAll(where: { stream in
                return oldUrns.contains(stream.device.descriptor().urn)
            })
        }
    }

    // A helper function to find a participant by its ID, mutate that participant, and then update the UICollectionView accordingly.
    private func mutatingParticipant(_ participantId: String?, modifier: (inout StageParticipant) -> Void) {
        guard let index = participants.firstIndex(where: { $0.participantId == participantId }) else {
            fatalError("Something is out of sync, investigate if this was a sample app or SDK issue.")
        }

        var participant = participants[index]
        modifier(&participant)
        participants[index] = participant
        participantsChanged(index: index, changeType: .updated)
    }
}
```

Ce code utilise une extension pour convertir l’état de connexion en texte convivial :

```
extension IVSStageConnectionState {
    var text: String {
        switch self {
        case .disconnected: return "Disconnected"
        case .connecting: return "Connecting"
        case .connected: return "Connected"
        @unknown default: fatalError()
        }
    }
}
```

## Implémentation d’un UICollectionViewLayout personnalisé
<a name="getting-started-pub-sub-ios-layout"></a>

La répartition des différents nombres de participants peut s’avérer complexe. Vous souhaitez qu’ils occupent l’intégralité du cadre de la vue parent, mais vous ne souhaitez pas gérer la configuration de chaque participant de manière indépendante. Pour vous faciliter la tâche, nous allons procéder à l’implémentation d’un `UICollectionViewLayout`.

Créez un autre fichier, `ParticipantCollectionViewLayout.swift`, qui étend `UICollectionViewLayout`. Cette classe utilisera une autre classe appelée `StageLayoutCalculator`, que nous aborderons bientôt. La classe reçoit les valeurs de trame calculées pour chaque participant et génère ensuite les objets `UICollectionViewLayoutAttributes` nécessaires.

```
import Foundation
import UIKit

/**
 Code modified from https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/layouts/customizing_collection_view_layouts?language=objc
 */
class ParticipantCollectionViewLayout: UICollectionViewLayout {

    private let layoutCalculator = StageLayoutCalculator()

    private var contentBounds = CGRect.zero
    private var cachedAttributes = [UICollectionViewLayoutAttributes]()

    override func prepare() {
        super.prepare()

        guard let collectionView = collectionView else { return }

        cachedAttributes.removeAll()
        contentBounds = CGRect(origin: .zero, size: collectionView.bounds.size)

        layoutCalculator.calculateFrames(participantCount: collectionView.numberOfItems(inSection: 0),
                                         width: collectionView.bounds.size.width,
                                         height: collectionView.bounds.size.height,
                                         padding: 4)
        .enumerated()
        .forEach { (index, frame) in
            let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: index, section: 0))
            attributes.frame = frame
            cachedAttributes.append(attributes)
            contentBounds = contentBounds.union(frame)
        }
    }

    override var collectionViewContentSize: CGSize {
        return contentBounds.size
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        guard let collectionView = collectionView else { return false }
        return !newBounds.size.equalTo(collectionView.bounds.size)
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cachedAttributes[indexPath.item]
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var attributesArray = [UICollectionViewLayoutAttributes]()

        // Find any cell that sits within the query rect.
        guard let lastIndex = cachedAttributes.indices.last, let firstMatchIndex = binSearch(rect, start: 0, end: lastIndex) else {
            return attributesArray
        }

        // Starting from the match, loop up and down through the array until all the attributes
        // have been added within the query rect.
        for attributes in cachedAttributes[..<firstMatchIndex].reversed() {
            guard attributes.frame.maxY >= rect.minY else { break }
            attributesArray.append(attributes)
        }

        for attributes in cachedAttributes[firstMatchIndex...] {
            guard attributes.frame.minY <= rect.maxY else { break }
            attributesArray.append(attributes)
        }

        return attributesArray
    }

    // Perform a binary search on the cached attributes array.
    func binSearch(_ rect: CGRect, start: Int, end: Int) -> Int? {
        if end < start { return nil }

        let mid = (start + end) / 2
        let attr = cachedAttributes[mid]

        if attr.frame.intersects(rect) {
            return mid
        } else {
            if attr.frame.maxY < rect.minY {
                return binSearch(rect, start: (mid + 1), end: end)
            } else {
                return binSearch(rect, start: start, end: (mid - 1))
            }
        }
    }
}
```

La classe `StageLayoutCalculator.swift` revêt une plus grande importance. Elle est conçue pour calculer les cadres de chaque participant en fonction du nombre de participants dans une disposition en ligne/colonne basée sur le flux. Chaque ligne a la même hauteur que les autres, mais les colonnes peuvent avoir des largeurs différentes par ligne. Voir le commentaire de code au-dessus de la variable `layouts` pour une description de la façon de personnaliser ce comportement.

```
import Foundation
import UIKit

class StageLayoutCalculator {

    /// This 2D array contains the description of how the grid of participants should be rendered
    /// The index of the 1st dimension is the number of participants needed to active that configuration
    /// Meaning if there is 1 participant, index 0 will be used. If there are 5 participants, index 4 will be used.
    ///
    /// The 2nd dimension is a description of the layout. The length of the array is the number of rows that
    /// will exist, and then each number within that array is the number of columns in each row.
    ///
    /// See the code comments next to each index for concrete examples.
    ///
    /// This can be customized to fit any layout configuration needed.
    private let layouts: [[Int]] = [
        // 1 participant
        [ 1 ], // 1 row, full width
        // 2 participants
        [ 1, 1 ], // 2 rows, all columns are full width
        // 3 participants
        [ 1, 2 ], // 2 rows, first row's column is full width then 2nd row's columns are 1/2 width
        // 4 participants
        [ 2, 2 ], // 2 rows, all columns are 1/2 width
        // 5 participants
        [ 1, 2, 2 ], // 3 rows, first row's column is full width, 2nd and 3rd row's columns are 1/2 width
        // 6 participants
        [ 2, 2, 2 ], // 3 rows, all column are 1/2 width
        // 7 participants
        [ 2, 2, 3 ], // 3 rows, 1st and 2nd row's columns are 1/2 width, 3rd row's columns are 1/3rd width
        // 8 participants
        [ 2, 3, 3 ],
        // 9 participants
        [ 3, 3, 3 ],
        // 10 participants
        [ 2, 3, 2, 3 ],
        // 11 participants
        [ 2, 3, 3, 3 ],
        // 12 participants
        [ 3, 3, 3, 3 ],
    ]

    // Given a frame (this could be for a UICollectionView, or a Broadcast Mixer's canvas), calculate the frames for each
    // participant, with optional padding.
    func calculateFrames(participantCount: Int, width: CGFloat, height: CGFloat, padding: CGFloat) -> [CGRect] {
        if participantCount > layouts.count {
            fatalError("Only \(layouts.count) participants are supported at this time")
        }
        if participantCount == 0 {
            return []
        }
        var currentIndex = 0
        var lastFrame: CGRect = .zero

        // If the height is less than the width, the rows and columns will be flipped.
        // Meaning for 6 participants, there will be 2 rows of 3 columns each.
        let isVertical = height > width

        let halfPadding = padding / 2.0

        let layout = layouts[participantCount - 1] // 1 participant is in index 0, so `-1`.
        let rowHeight = (isVertical ? height : width) / CGFloat(layout.count)

        var frames = [CGRect]()
        for row in 0 ..< layout.count {
            // layout[row] is the number of columns in a layout
            let itemWidth = (isVertical ? width : height) / CGFloat(layout[row])
            let segmentFrame = CGRect(x: (isVertical ? 0 : lastFrame.maxX) + halfPadding,
                                      y: (isVertical ? lastFrame.maxY : 0) + halfPadding,
                                      width: (isVertical ? itemWidth : rowHeight) - padding,
                                      height: (isVertical ? rowHeight : itemWidth) - padding)

            for column in 0 ..< layout[row] {
                var frame = segmentFrame
                if isVertical {
                    frame.origin.x = (itemWidth * CGFloat(column)) + halfPadding
                } else {
                    frame.origin.y = (itemWidth * CGFloat(column)) + halfPadding
                }
                frames.append(frame)
                currentIndex += 1
            }

            lastFrame = segmentFrame
            lastFrame.origin.x += halfPadding
            lastFrame.origin.y += halfPadding
        }
        return frames
    }

}
```

De retour à `Main.storyboard`, veillez à utiliser la classe que nous venons de créer pour définir la classe de disposition de `UICollectionView` :

![\[Xcode interface showing storyboard with UICollectionView and its layout settings.\]](http://docs.aws.amazon.com/fr_fr/ivs/latest/RealTimeUserGuide/images/Publish_iOS_12.png)


## Connexion des actions de l’interface utilisateur
<a name="getting-started-pub-sub-ios-actions"></a>

Nous nous rapprochons, il nous reste quelques `IBActions` à créer.

Nous allons d’abord gérer le bouton Rejoindre. Il répond différemment en fonction de la valeur de `connectingOrConnected`. Lorsqu’il est déjà connecté, il a pour effet de quitter la scène. S’il est déconnecté, il lit le texte du jeton `UITextField` et crée un `IVSStage` avec ce texte. Ensuite, nous ajoutons notre `ViewController` en tant que `strategy`, `errorDelegate`, et moteur de rendu pour `IVSStage`, et enfin nous rejoignons la scène de manière asynchrone.

```
@IBAction private func joinTapped(_ sender: UIButton) {
    if connectingOrConnected {
        // If we're already connected to a Stage, leave it.
        stage?.leave()
    } else {
        guard let token = textFieldToken.text else {
            print("No token")
            return
        }
        // Hide the keyboard after tapping Join
        textFieldToken.resignFirstResponder()
        do {
            // Destroy the old Stage first before creating a new one.
            self.stage = nil
            let stage = try IVSStage(token: token, strategy: self)
            stage.errorDelegate = self
            stage.addRenderer(self)
            try stage.join()
            self.stage = stage
        } catch {
            print("Failed to join stage - \(error)")
        }
    }
}
```

L’autre action de l’interface utilisateur que nous devons connecter est le commutateur de publication :

```
@IBAction private func publishToggled(_ sender: UISwitch) {
    // Because the strategy returns the value of `switchPublish.isOn`, just call `refreshStrategy`.
    stage?.refreshStrategy()
}
```

## Affichage des participants
<a name="getting-started-pub-sub-ios-participants"></a>

Enfin, nous devons rendre les données que nous recevons du SDK sur la cellule de participant que nous avons créée précédemment. La logique du `UICollectionView` est déjà terminée, il ne nous reste plus qu’à implémenter l’API `set` dans `ParticipantCollectionViewCell.swift`.

Nous allons commencer par ajouter la fonction `empty`, puis nous l’étudierons étape par étape :

```
func set(participant: StageParticipant) {
   
}
```

Nous gérons d’abord l’état simplifié, l’identifiant du participant, l’état de publication et l’état d’abonnement. Pour ceux-ci, nous mettons simplement à jour nos `UILabels` directement :

```
labelParticipantId.text = participant.isLocal ? "You (\(participant.participantId ?? "Disconnected"))" : participant.participantId
labelPublishState.text = participant.publishState.text
labelSubscribeState.text = participant.subscribeState.text
```

Les propriétés de texte des énumérations de publication et d’abonnement proviennent d’extensions locales :

```
extension IVSParticipantPublishState {
    var text: String {
        switch self {
        case .notPublished: return "Not Published"
        case .attemptingPublish: return "Attempting to Publish"
        case .published: return "Published"
        @unknown default: fatalError()
        }
    }
}

extension IVSParticipantSubscribeState {
    var text: String {
        switch self {
        case .notSubscribed: return "Not Subscribed"
        case .attemptingSubscribe: return "Attempting to Subscribe"
        case .subscribed: return "Subscribed"
        @unknown default: fatalError()
        }
    }
}
```

Ensuite, nous mettons à jour les états de coupure de l’audio et de la vidéo. Pour obtenir les états de coupure, nous devons trouver `IVSImageDevice` et `IVSAudioDevice` à partir du tableau de `streams`. Pour optimiser les performances, nous mémorisons les derniers identifiants des appareils connectés.

```
// This belongs outside `set(participant:)`
private var registeredStreams: Set<IVSStageStream> = []
private var imageDevice: IVSImageDevice? {
    return registeredStreams.lazy.compactMap { $0.device as? IVSImageDevice }.first
}
private var audioDevice: IVSAudioDevice? {
    return registeredStreams.lazy.compactMap { $0.device as? IVSAudioDevice }.first
}

// This belongs inside `set(participant:)`
let existingAudioStream = registeredStreams.first { $0.device is IVSAudioDevice }
let existingImageStream = registeredStreams.first { $0.device is IVSImageDevice }

registeredStreams = Set(participant.streams)

let newAudioStream = participant.streams.first { $0.device is IVSAudioDevice }
let newImageStream = participant.streams.first { $0.device is IVSImageDevice }

// `isMuted != false` covers the stream not existing, as well as being muted.
labelVideoMuted.text = "Video Muted: \(newImageStream?.isMuted != false)"
labelAudioMuted.text = "Audio Muted: \(newAudioStream?.isMuted != false)"
```

Enfin, nous voulons afficher un aperçu du `imageDevice` et affichez les statistiques audio du `audioDevice` :

```
if existingImageStream !== newImageStream {
    // The image stream has changed
    updatePreview() // We’ll cover this next
}

if existingAudioStream !== newAudioStream {
    (existingAudioStream?.device as? IVSAudioDevice)?.setStatsCallback(nil)
    audioDevice?.setStatsCallback( { [weak self] stats in
        self?.labelAudioVolume.text = String(format: "Audio Level: %.0f dB", stats.rms)
    })
    // When the audio stream changes, it will take some time to receive new stats. Reset the value temporarily.
    self.labelAudioVolume.text = "Audio Level: -100 dB"
}
```

La dernière fonction que nous devons créer est `updatePreview()`, qui ajoute un aperçu du participant à notre vue :

```
private func updatePreview() {
    // Remove any old previews from the preview container
    viewPreviewContainer.subviews.forEach { $0.removeFromSuperview() }
    if let imageDevice = self.imageDevice {
        if let preview = try? imageDevice.previewView(with: .fit) {
            viewPreviewContainer.addSubviewMatchFrame(preview)
        }
    }
}
```

Ce qui précède utilise une fonction d’assistance sur `UIView` pour faciliter l’intégration de sous-vues :

```
extension UIView {
    func addSubviewMatchFrame(_ view: UIView) {
        view.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(view)
        NSLayoutConstraint.activate([
            view.topAnchor.constraint(equalTo: self.topAnchor, constant: 0),
            view.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0),
            view.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0),
            view.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0),
        ])
    }
}
```