

# Schritt 5: Video veröffentlichen und abonnieren
<a name="getting-started-pub-sub"></a>

Sie können Folgendes nutzen, um in IVS zu veröffentlichen/abonnieren (in Echtzeit):
+ Die nativen [IVS-Broadcast-SDKs](https://docs.aws.amazon.com//ivs/latest/LowLatencyUserGuide/getting-started-set-up-streaming.html#broadcast-sdk), die WebRTC und RTMPS unterstützen. Wir empfehlen dies insbesondere für Produktionsszenarien. Nachfolgend finden Sie die Details für [Web](getting-started-pub-sub-web.md), [Android](getting-started-pub-sub-android.md) und [iOS](getting-started-pub-sub-ios.md).
+ Die Amazon IVS-Konsole – Diese eignet sich zum Testen von Streams. Weitere Informationen finden Sie unter weiter unten in diesem Dokument.
+ Andere Software- und Hardware-Encoder für das Streaming – Sie können alle Streaming-Encoder verwenden, die die Protokolle RTMP, RTMPS oder WHIP unterstützen. Weitere Informationen finden Sie unter [Stream-Erfassung](rt-stream-ingest.md).

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

1. Öffnen Sie die [Amazon-IVS-Konsole](https://console.aws.amazon.com/ivs).

   (Sie können auf die Amazon IVS Konsole auch über die [AWS-Managementkonsole](https://console.aws.amazon.com/) zugreifen.)

1. Wählen Sie im Navigationsbereich die Option **Stage** aus. (Wenn der Navigationsbereich ausgeblendet ist, erweitern Sie ihn über das Hamburger-Symbol.)

1. Wählen Sie die Stage aus, die Sie abonnieren oder veröffentlichen möchten, um ihre Detailseite aufzurufen.

1. Abonnieren: Wenn die Stage einen oder mehrere Publisher hat, können Sie sie abonnieren, indem Sie auf der Registerkarte **Abonnieren** auf die Schaltfläche **Abonnieren** klicken. Die Registerkarte wird unter dem Abschnitt **Allgemeine Konfiguration** angezeigt.

1. Zum Veröffentlichen:

   1. Wählen Sie die Registerkarte **Veröffentlichen** aus.

   1. Sie werden aufgefordert, der IVS-Konsole Zugriff auf Ihre Kamera und Ihr Mikrofon zu gewähren. **Erlauben** Sie diese Berechtigungen.

   1. Wählen Sie unten auf der Registerkarte **Veröffentlichen** mit den Dropdown-Feldern die Eingabegeräte für das Mikrofon und die Kamera aus.

   1. Um mit der Veröffentlichung zu beginnen, wählen Sie **Veröffentlichung starten** aus.

   1. Um Ihre veröffentlichten Inhalte anzuzeigen, kehren Sie zur Registerkarte **Abonnieren** zurück.

   1. Um die Veröffentlichung zu beenden, klicken Sie auf der Registerkarte **Veröffentlichen** unten auf die Schaltfläche **Veröffentlichung beenden**.

**Hinweis**: Das Abonnieren und Veröffentlichen verbraucht Ressourcen, und für die Zeit, in der Sie mit der Stage verbunden sind, wird ein Stundensatz berechnet. Weitere Informationen finden Sie auf der Seite mit den IVS-Preisen unter [Echtzeit-Streaming](https://aws.amazon.com/ivs/pricing/#Real-Time_Streaming).

# Mit dem IVS Web Broadcast SDK veröffentlichen und abonnieren
<a name="getting-started-pub-sub-web"></a>

Dieser Abschnitt führt Sie durch die Schritte zur Veröffentlichung und zum Abonnieren einer Stage mithilfe Ihrer Web-App.

## HTML-Boilerplate erstellen
<a name="getting-started-pub-sub-web-html"></a>

Lassen Sie uns zunächst das HTML-Boilerplate erstellen und die Bibliothek als Script-Tag importieren:

```
<!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>
```

## Token-Eingabe akzeptieren und Schaltflächen zum Beitritten/Verlassen hinzufügen
<a name="getting-started-pub-sub-web-join"></a>

Hier füllen wir den Hauptteil mit unseren Eingabekontrollen aus. Diese nehmen das Token als Eingabe und richten Schaltflächen für **Beitreten** und **Verlassen** ein. Normalerweise fordern Anwendungen das Token von der API Ihrer Anwendung an, aber in diesem Beispiel kopieren Sie das Token und fügen es in die Token-Eingabe ein.

```
<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 />
```

## Mediencontainer-Elemente hinzufügen
<a name="getting-started-pub-sub-web-media"></a>

Diese Elemente werden die Medien für unsere lokalen und externen Teilnehmer bereitstellen. Wir fügen ein Script-Tag hinzu, um die in `app.js` definierte Logik unserer Anwendung zu laden.

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

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

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

Damit ist die HTML-Seite fertig. Sie sollten sie sehen, wenn Sie `index.html` in einem Browser laden:

![\[Echtzeit-Streaming in einem Browser anzeigen: Die HTML-Setup ist abgeschlossen.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/RT_Browser_View.png)


## Erstellen von app.js
<a name="getting-started-pub-sub-web-appjs"></a>

Gehen wir zur Definition des Inhalts unserer `app.js`-Datei. Importieren Sie zunächst alle erforderlichen Eigenschaften aus der globalen Version des SDK:

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

## Anwendungsvariablen erstellen
<a name="getting-started-pub-sub-web-vars"></a>

Erstellen Sie Variablen, um Verweise auf unsere HTML-Elemente für die Schaltflächen **Beitreten** und **Verlassen** zu speichern und den Status für die Anwendung zu speichern:

```
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;
```

## JoinStage 1 erstellen: Definieren Sie die Funktion und validieren Sie die Eingabe
<a name="getting-started-pub-sub-web-joinstage1"></a>

Die Funktion `joinStage` nimmt das Eingabe-Token, stellt eine Verbindung zur Stage her und beginnt mit der Veröffentlichung von Video- und Audiodaten, die von `getUserMedia` empfangen werden.

Zu Beginn definieren wir die Funktion und validieren den Status und die Token-Eingabe. Wir werden diese Funktion in den nächsten Abschnitten näher erläutern.

```
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
};
```

## JoinStage 2 erstellen: Medien zum Veröffentlichen abrufen
<a name="getting-started-pub-sub-web-joinstage2"></a>

Hier sind die Medien, die auf der Stage veröffentlicht werden:

```
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]);
```

## JoinStage 3 erstellen: Definieren Sie die Stagestrategie und erstellen Sie die Stage
<a name="getting-started-pub-sub-web-joinstage3"></a>

Diese Stagestrategie ist das Herzstück der Entscheidungslogik, anhand derer das SDK entscheidet, was veröffentlicht und welche Teilnehmer abonniert werden sollen. Weitere Informationen zum Zweck der Funktion finden Sie unter [Strategie](web-publish-subscribe.md#web-publish-subscribe-concepts-strategy).

Diese Strategie ist einfach. Nachdem Sie die Stage betreten haben, veröffentlichen Sie die soeben abgerufenen Streams und abonnieren die Audio- und Videodaten aller Remote-Teilnehmer:

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

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

## JoinStage 4 erstellen: Stageereignisse verarbeiten und Medien rendern
<a name="getting-started-pub-sub-web-joinstage4"></a>

Stages geben viele Ereignisse ab. Wir müssen auf die `STAGE_PARTICIPANT_STREAMS_ADDED` und `STAGE_PARTICIPANT_LEFT` hören, um Medien auf und von der Seite zu rendern und zu entfernen. Eine umfassendere Reihe von Ereignissen finden Sie unter [Ereignisse](web-publish-subscribe.md#web-publish-subscribe-concepts-events).

Beachten Sie, dass wir hier vier Hilfsfunktionen erstellen, die uns bei der Verwaltung der erforderlichen DOM-Elemente unterstützen:`setupParticipant`, `teardownParticipant`, `createVideoEl` und `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;
}
```

## JoinStage 5 erstellen: Treten Sie der Stage bei
<a name="getting-started-pub-sub-web-joinstage5"></a>

Vervollständigen wir unsere Funktion `joinStage` , indem Sie endlich die Stage betreten\$1

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

## LeaveStage erstellen
<a name="getting-started-pub-sub-web-leavestage"></a>

Definieren Sie die `leaveStage`-Funktion, die die Verlassen-Schaltfläche aufrufen wird.

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

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

## Input-Event-Handler initialisieren
<a name="getting-started-pub-sub-web-handlers"></a>

Wir fügen eine letzte Funktion zu unserer `app.js`-Datei hinzu. Diese Funktion wird sofort aufgerufen, wenn die Seite geladen wird, und richtet Event-Handler für den Beitritt und das Verlassen der Stage ein.

```
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
```

## Führen Sie die Anwendung aus und geben Sie ein Token an
<a name="getting-started-pub-sub-run-app"></a>

Jetzt können Sie die Webseite lokal oder mit anderen teilen, [die Seite öffnen](#getting-started-pub-sub-web-media), ein Teilnehmer-Token eingeben und der Stage beitreten.

## Die nächsten Themen
<a name="getting-started-pub-sub-next"></a>

Ausführlichere Beispiele für npm, React und mehr finden Sie in [IVS-Broadcast-SDK: Web-Leitfaden (Anleitung zu Echtzeit-Streaming)](broadcast-web.md).

# Mit dem IVS Android Broadcast SDK veröffentlichen und abonnieren
<a name="getting-started-pub-sub-android"></a>

Dieser Abschnitt führt Sie durch die Schritte zur Veröffentlichung und zum Abonnieren einer Stage mithilfe Ihrer Android-App.

## Ansichten erstellen
<a name="getting-started-pub-sub-android-views"></a>

Wir beginnen mit der Erstellung eines einfachen Layouts für unsere Anwendung mithilfe der automatisch erstellten `activity_main.xml`-Datei. Das Layout enthält einen `EditText`, um ein Token hinzuzufügen, einen Beitreten-`Button`, eine `TextView`, um den Status der Stage anzuzeigen, und eine `CheckBox`, um die Veröffentlichung umzuschalten.

![\[Richten Sie das Veröffentlichungslayout für Ihre Android-Anwendung ein.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_Android_1.png)


Hier ist das XML hinter der Ansicht:

```
<?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>
```

Wir haben hier auf ein paar String-IDs verwiesen, also erstellen wir unsere gesamte `strings.xml`-Datei jetzt:

```
<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>
```

Lassen Sie uns diese Ansichten im XML mit unseren `MainActivity.kt` verknüpfen:

```
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)
}
```

Jetzt erstellen wir eine Elementansicht für unsere `RecyclerView`. Klicken Sie dazu mit der rechten Maustaste auf `res/layout`-Verzeichnis und wählen Sie **Neu > Layout-Ressourcendatei**. Benennen Sie diese Datei `item_stage_participant.xml`.

![\[Erstellen Sie eine Elementansicht für Ihre Android-App-RecyclerView.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_Android_2.png)


Das Layout für dieses Element ist einfach: Es enthält eine Ansicht zum Rendern des Videostreams eines Teilnehmers und eine Liste von Labels zur Anzeige von Informationen über den Teilnehmer:

![\[Erstellen Sie eine Elementansicht für Ihre Android-App-RecyclerView – Label.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_Android_3.png)


Hier ist das 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>
```

Diese XML-Datei generiert eine Klasse, die wir noch nicht erstellt haben, `ParticipantItem`. Da das XML den vollständigen Namespace enthält, sollten Sie diese XML-Datei unbedingt in Ihren Namespace aktualisieren. Lassen Sie uns diese Klasse erstellen und die Ansichten einrichten, aber ansonsten lassen wir das Feld vorerst leer.

Erstellen Sie eine neue Kotlin-Klasse, `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)
    }
}
```

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

Um die Kamera und das Mikrofon verwenden zu können, müssen Sie vom Benutzer Berechtigungen anfordern. Dafür folgen wir einem standardmäßigen Berechtigungsablauf:

```
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
    }
}
```

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

Unsere Anwendung verfolgt die Teilnehmer vor Ort in einer `MainViewModel.kt` und der Status wird an die `MainActivity` zurückgemeldet mit Kotlins [StateFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/).

Erstellen Sie eine neue Kotlin-Klasse `MainViewModel`:

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

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

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

}
```

In `MainActivity.kt` verwalten wir unser Ansicht-Modell:

```
import androidx.activity.viewModels

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

Um `AndroidViewModel` und diese Kotlin-`ViewModel`-Erweiterungen zu verwenden, müssen Sie Folgendes zur `build.gradle`-Datei Ihres Moduls hinzufügen:

```
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>

Wir werden eine einfache `RecyclerView.Adapter`-Unterklasse erstellen, um unsere Teilnehmer zu verfolgen und unsere `RecyclerView` auf Stageereignisse zu aktualisieren. Aber zuerst brauchen wir eine Klasse, die einen Teilnehmer repräsentiert. Erstellen Sie eine neue Kotlin-Klasse `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)
            }
        }
}
```

Wir verwenden diese Klasse in der `ParticipantAdapter`-Klasse, die wir als Nächstes erstellen werden. Wir beginnen damit, die Klasse zu definieren und eine Variable zu erstellen, um die Teilnehmer zu verfolgen:

```
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>()
```

Wir müssen auch unsere `RecyclerView.ViewHolder` definieren bevor Sie die restlichen Überschreibungen implementieren:

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

Damit können wir die Standard-`RecyclerView.Adapter`-Überschreibung implementieren:

```
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)
    }
}
```

Schließlich fügen wir neue Methoden hinzu, die wir von unserem `MainViewModel` abrufen, wenn Änderungen an den Teilnehmern vorgenommen werden. Bei diesen Methoden handelt es sich um Standard-CRUD-Operationen auf dem Adapter.

```
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])
    }
}
```

Wieder in `MainViewModel` müssen wir einen Verweis auf diesen Adapter erstellen und speichern:

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

## Stage-Status
<a name="getting-started-pub-sub-android-views-stage-state"></a>

Wir müssen auch einige Stagestatus innerhalb `MainViewModel` verfolgen. Definieren wir jetzt diese Eigenschaften:

```
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>()
```

Um Ihre eigene Vorschau zu sehen, bevor Sie eine Stage betreten, erstellen wir sofort einen lokalen Teilnehmer:

```
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)
}
```

Wir wollen sicherstellen, dass wir diese Ressourcen bereinigen, wenn unsere `ViewModel` bereinigt ist. Wir überschreiben `onCleared()` sofort, damit wir nicht vergessen, diese Ressourcen zu reinigen.

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

Jetzt füllen wir unsere lokale `streams`-Eigenschaft auf, sobald die Berechtigungen erteilt sind, und implementieren die `permissionsGranted`-Methode, die wir zuvor aufgerufen haben:

```
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)
    }
}
```

## Implementierung des Stage-SDK
<a name="getting-started-pub-sub-android-stage-sdk"></a>

Drei [Kernkonzepte](android-publish-subscribe.md#android-publish-subscribe-concepts) liegen der Echtzeit-Funktionalität zugrunde: Stage, Strategie und Renderer. Das Designziel besteht in der Minimierung der Menge an clientseitiger Logik, die für die Entwicklung eines funktionierenden Produkts erforderlich ist.

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

Die `Stage.Strategy`-Implementierung ist einfach:

```
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
}
```

Zusammenfassend lässt sich sagen, dass wir auf der Grundlage unseres internen Status `publishEnabled` veröffentlichen, und wenn wir veröffentlichen, veröffentlichen wir die zuvor gesammelten Streams. Schließlich abonnieren wir für dieses Beispiel immer andere Teilnehmer und erhalten sowohl ihr Audio als auch ihr Video.

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

Die `StageRenderer`-Implementierung ist ebenfalls ziemlich einfach, obwohl sie angesichts der Anzahl der Funktionen reichlich mehr Code enthält. Der allgemeine Ansatz in diesem Renderer besteht darin, unseren `ParticipantAdapter` zu aktualisieren, wenn das SDK uns über eine Änderung an einem Teilnehmer informiert. Es gibt bestimmte Szenarien, in denen wir mit lokalen Teilnehmern anders umgehen, weil wir beschlossen haben, sie selbst zu verwalten, sodass sie ihre Kameravorschau sehen können, bevor sie beitreten.

```
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) {}
}
```

## Implementierung eines benutzerdefinierten RecyclerView-LayoutManagers
<a name="getting-started-pub-sub-android-layout"></a>

Die Festlegung verschiedener Teilnehmerzahlen kann komplex sein. Sie möchten, dass sie den gesamten Frame der übergeordneten Ansicht einnehmen, aber Sie möchten nicht jede Teilnehmerkonfiguration unabhängig voneinander handhaben. Um dies zu vereinfachen, führen wir die Implementierung eines `RecyclerView.LayoutManager` durch.

Erstelle eine weitere neue Klasse `StageLayoutManager`, welche `GridLayoutManager` erweitern soll. In diesem Kurs wird das Layout für jeden Teilnehmer anhand der Anzahl der Teilnehmer in einem flussbasierten Zeilen-/Spaltenlayout berechnet. Jede Zeile hat dieselbe Höhe wie die anderen, aber Spalten können pro Zeile unterschiedlich breit sein. Sehen Sie den Code-Kommentar über der `layouts`-Variable für eine Beschreibung, wie dieses Verhalten angepasst werden kann.

```
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
}
```

Wieder zurück in `MainActivity.kt` müssen wir den Adapter und den Layoutmanager für unsere `RecyclerView` einrichten:

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

## UI-Aktionen verbinden
<a name="getting-started-pub-sub-android-actions"></a>

Wir sind nah dran; es gibt nur ein paar UI-Aktionen, die wir verbinden müssen.

Zuerst haben wir unsere `MainActivity`, die die `StateFlow`-Änderungen von`MainViewModel` beobachtet:

```
// 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)
        }
    }
}
```

Als Nächstes fügen wir Listener zu unseren Schaltflächen „Beitreten“ und „Veröffentlichen“ hinzu:

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

Beide der oben genannten Anruffunktionen in unserer `MainViewModel`, die wir jetzt implementieren:

```
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
}
```

## Rendern der Teilnehmer
<a name="getting-started-pub-sub-android-participants"></a>

Schließlich müssen wir die Daten, die wir vom SDK erhalten, auf das zuvor erstellte Teilnehmerelement übertragen. Wir haben die Logik von `RecyclerView` bereits fertig, also müssen wir nur noch die API von `bind` in `ParticipantItem` implementieren.

Wir beginnen mit dem Hinzufügen der leeren Funktion und gehen sie dann Schritt für Schritt durch:

```
fun bind(participant: StageParticipant) {

}
```

Zuerst kümmern wir uns um den Status „Einfach“, die Teilnehmer-ID, den Veröffentlichungsstatus und den Abonnementstatus. Für diese aktualisieren wir einfach unsere `TextViews` direkt:

```
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
```

Als Nächstes aktualisieren wir die stummgeschalteten Audio- und Videozustände. Um den Stummschaltzustand zu erhalten, müssen wir den `ImageDevice` und `AudioDevice` aus dem Streams-Array finden. Um die Leistung zu optimieren, erinnern wir uns an die zuletzt angehängten Geräte-IDs.

```
// 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"
}
```

Abschließend wollen wir eine Vorschau für das `imageDevice` rendern:

```
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
```

Und wir zeigen Audiostatistiken von der `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
```

# Mit dem IVS iOS Broadcast SDK veröffentlichen und abonnieren
<a name="getting-started-pub-sub-ios"></a>

Dieser Abschnitt führt Sie durch die Schritte zur Veröffentlichung und zum Abonnieren einer Stage mithilfe Ihrer iOS-App.

## Ansichten erstellen
<a name="getting-started-pub-sub-ios-views"></a>

Wir beginnen mit der automatisch erstellten `ViewController.swift`-Datei, um `AmazonIVSBroadcast` zu importieren und dann fügen wir etwas `@IBOutlets` hinzu zum Verlinken:

```
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!
```

Jetzt erstellen wir diese Ansichten und verknüpfen sie in `Main.storyboard`. Hier ist die Ansichtsstruktur, die wir verwenden werden:

![\[Verwenden Sie Main.storyboard, um eine iOS-Ansicht zu erstellen.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_iOS_1.png)


Für die AutoLayout-Konfiguration müssen wir drei Ansichten anpassen. Die erste Ansicht ist **Sammlungsansicht der Teilnehmer** (ein `UICollectionView`). Binden Sie **Führend**, **Verfolgend**, und **Unterseite** zu **Sicherer Bereich**. Binden Sie auch **Oben** zu **Steuert Container**.

![\[Passen Sie die iOS-Sammlungsansicht der Teilnehmer an.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_iOS_2.png)


Die zweite Ansicht ist **Steuert Container**. Binden Sie **Führend**, **Verfolgend**, und **Unterseite** zu **Sicherer Bereich**:

![\[Passen Sie die iOS-Ansicht „Steuert Container“ an.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_iOS_3.png)


Die dritte und letzte Ansicht ist **Vertikale Stapelansicht**. Binden Sie **Oben**,**Führend**,**Verfolgend**, und **Unterseite** zu **Superansicht**. Stellen Sie für das Styling den Abstand auf 8 statt auf 0 ein.

![\[Passen Sie die vertikale Stack-Ansicht von iOS an.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_iOS_4.png)


**UIStack-Ansichten** kümmert sich um das Layout der verbleibenden Ansichten. Für alle drei **UIStack-Ansichten**, verwenden Sie **Füllen**, sowie **Ausrichtung** und **Verteilung**.

![\[Passen Sie die verbleibenden iOS-Ansichten mit UIStack-Ansichten an.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_iOS_5.png)


Lassen Sie uns abschließend diese Ansichten mit unserem `ViewController` verknüpfen. Kartieren Sie von oben die folgenden Ansichten:
+ **Textfeld-Verknüpfung** bindet an `textFieldToken`.
+ **Schaltfläche Beitreten** bindet an `buttonJoin`.
+ **Status beschriften** bindet an `labelState`.
+ **Veröffentlichen wechseln** bindet an `switchPublish`.
+ **Sammlungsansicht der Teilnehmer** bindet an `collectionViewParticipants`.

Nutzen Sie diese Zeit auch, um das `dataSource` des Elements **Sammlungsansicht der Teilnehmer** auf den Besitz von `ViewController` einzustellen:

![\[Legen Sie die dataSource der Anwendung „Sammlungsansicht der Teilnehmer“ für iOS fest.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_iOS_6.png)


Jetzt erstellen wir die `UICollectionViewCell`-Unterklasse, in welcher die Teilnehmer gerendert werden sollen. Erstellen Sie zunächst eine neue **Cocoa-Touch-Class**-Datei:

![\[Erstellen Sie eine UICollectionViewCell, um iOS-Teilnehmer in Echtzeit zu rendern.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_iOS_7.png)


Nennen Sie sie `ParticipantUICollectionViewCell` und machen Sie es zu einer Unterklasse von `UICollectionViewCell` in Swift. Wir beginnen erneut in der Swift-Datei und erstellen unsere `@IBOutlets` zum Verlinken:

```
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!
```

Erstellen Sie in der zugehörigen XIB-Datei diese Ansichtshierarchie:

![\[Erstellen Sie die Hierarchie der iOS-Ansicht in der zugehörigen XIB-Datei.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_iOS_8.png)


Für AutoLayout ändern wir erneut drei Ansichten. Die erste Ansicht ist **Vorschaucontainer anzeigen**. Setzen Sie **Verfolgend**, **Führend**, **Oben** und **Unterseite** zu **Sammlungsansicht der Teilnehmer Zelle**.

![\[Passen Sie die Vorschau der Container-Ansicht in der iOS-Ansicht an.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_iOS_9.png)


Die zweite Ansicht ist **Ansicht**. Setzen Sie **Führend** und **Oben**  zu **Sammlungsansicht der Teilnehmer Zelle** und ändern Sie den Wert auf 4.

![\[Passen Sie die iOS-Ansicht an.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_iOS_10.png)


Die dritte Ansicht ist **Stapelansicht**. Setzen Sie **Verfolgend**, **Führend**, **Oben** und **Unten** auf **Superansicht** und ändern Sie den Wert auf 4.

![\[Passen Sie die iOS-Stack-Ansicht an.\]](http://docs.aws.amazon.com/de_de/ivs/latest/RealTimeUserGuide/images/Publish_iOS_11.png)


## Berechtigungen und Idle Timer
<a name="getting-started-pub-sub-ios-perms"></a>

Zurück zu unserem `ViewController`. Wir werden den System-Leerlauf-Timer deaktivieren, um zu verhindern, dass das Gerät in den Ruhemodus wechselt, während unsere Anwendung verwendet wird:

```
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
}
```

Als Nächstes fordern wir Kamera- und Mikrofonberechtigungen vom System an:

```
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)
    }
}
```

## Anwendungsstatus
<a name="getting-started-pub-sub-ios-app-state"></a>

Wir müssen unsere `collectionViewParticipants` konfigurieren mit der Layout-Datei, die wir zuvor erstellt haben:

```
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")
}
```

Um jeden Teilnehmer zu repräsentieren, erstellen wir eine einfache Struktur namens `StageParticipant`. Diese kann enthalten sein in der `ViewController.swift`-Datei, oder es kann eine neue Datei erstellt werden.

```
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
    }
}
```

Um diese Teilnehmer zu verfolgen, verwahren wir eine Reihe von ihnen als Privateigentum in unserem `ViewController`:

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

Diese Eigenschaft wird verwendet, um unsere `UICollectionViewDataSource` anzutreiben, die  früher  vom Storyboard aus verlinkt wurde:

```
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'")
        }
    }

}
```

Um Ihre eigene Vorschau zu sehen, bevor Sie eine Stage betreten, erstellen wir sofort einen lokalen Teilnehmer:

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

Dies führt dazu, dass sofort nach dem Ausführen der App eine Teilnehmerzelle gerendert wird, die den lokalen Teilnehmer darstellt.

Die Benutzer möchten sich selbst sehen können, bevor sie einer Stage beitreten. Deshalb implementieren wir als Nächstes die `setupLocalUser()`-Methode, die zuvor aus dem Code zur Bearbeitung von Berechtigungen aufgerufen wurde. Wir speichern die Kamera- und Mikrofonreferenz als `IVSLocalStageStream`-Objekte.

```
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)
}
```

Hier haben wir die Kamera und das Mikrofon des Geräts über das SDK gefunden und sie in unserem lokalen `streams`-Objekt gespeichert, dann zum `streams`-Array des ersten Teilnehmers (des lokalen Teilnehmers, den wir zuvor erstellt haben) zu unserem `streams` zugewiesen. Schließlich rufen wir `participantsChanged` mit einem `index` von 0 und `changeType` von `updated` auf. Diese Funktion ist eine Hilfsfunktion für die Aktualisierung unserer `UICollectionView` mit hübschen Animationen. So sieht es aus:

```
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)])
    }
}
```

Machen Sie sich jetzt keine Sorgen über `cell.set`. Darauf kommen wir später zurück, aber dort werden wir den Inhalt der Zelle basierend auf dem Teilnehmer rendern.

Der `ChangeType` ist eine einfache Aufzählung:

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

Schließlich möchten wir verfolgen, ob die Stage angeschlossen ist. Wir verwenden eine einfache `bool`, um das zu verfolgen, wodurch unsere Benutzeroberfläche automatisch aktualisiert wird, wenn sie selbst aktualisiert wird.

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

## Implementierung des Stage-SDK
<a name="getting-started-pub-sub-ios-stage-sdk"></a>

Drei [Kernkonzepte](ios-publish-subscribe.md#ios-publish-subscribe-concepts) liegen der Echtzeit-Funktionalität zugrunde: Stage, Strategie und Renderer. Das Designziel besteht in der Minimierung der Menge an clientseitiger Logik, die für die Entwicklung eines funktionierenden Produkts erforderlich ist.

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

Die `IVSStageStrategy`-Implementierung ist einfach:

```
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
    }
}
```

Zusammenfassend lässt sich sagen, dass wir nur veröffentlichen, wenn die Option „Veröffentlichen“ aktiviert ist, und wenn wir veröffentlichen, veröffentlichen wir die Streams, die wir zuvor gesammelt haben. Schließlich abonnieren wir für dieses Beispiel immer andere Teilnehmer und erhalten sowohl ihr Audio als auch ihr Video.

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

Die `IVSStageRenderer`-Implementierung ist ebenfalls ziemlich einfach, obwohl sie angesichts der Anzahl der Funktionen reichlich mehr Code enthält. Der allgemeine Ansatz in diesem Renderer besteht darin, unser `participants`-Array zu aktualisieren, wenn das SDK uns über eine Änderung an einen Teilnehmer informiert. Es gibt bestimmte Szenarien, in denen wir mit lokalen Teilnehmern anders umgehen, weil wir beschlossen haben, sie selbst zu verwalten, sodass sie ihre Kameravorschau sehen können, bevor sie beitreten.

```
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)
    }
}
```

Dieser Code verwendet eine Erweiterung, um den Verbindungsstatus in menschenfreundlichen Text umzuwandeln:

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

## Implementierung eines benutzerdefinierten UICollectionViewLayouts
<a name="getting-started-pub-sub-ios-layout"></a>

Die Festlegung verschiedener Teilnehmerzahlen kann komplex sein. Sie möchten, dass sie den gesamten Frame der übergeordneten Ansicht einnehmen, aber Sie möchten nicht jede Teilnehmerkonfiguration unabhängig voneinander handhaben. Um dies zu vereinfachen, führen wir die Implementierung eines `UICollectionViewLayout` durch.

Erstellen Sie eine weitere neue Datei, `ParticipantCollectionViewLayout.swift`, welche `UICollectionViewLayout` erweitern soll. Diese Klasse verwendet eine andere Klasse namens `StageLayoutCalculator`, was wir bald behandeln werden. Die Klasse erhält berechnete Rahmenwerte für jeden Teilnehmer und generiert dann die notwendigen `UICollectionViewLayoutAttributes`-Objekte.

```
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))
            }
        }
    }
}
```

Wichtiger ist die `StageLayoutCalculator.swift`-Klasse. Es ist so konzipiert, dass es die Rahmen für jeden Teilnehmer auf der Grundlage der Anzahl der Teilnehmer in einem fließenden Zeilen-/Spaltenlayout berechnet. Jede Zeile hat dieselbe Höhe wie die anderen, aber Spalten können pro Zeile unterschiedlich breit sein. Sehen Sie den Code-Kommentar über der `layouts`-Variable für eine Beschreibung, wie dieses Verhalten angepasst werden kann.

```
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
    }

}
```

Wieder in `Main.storyboard`, stellen Sie sicher, dass Sie die Layoutklasse für die `UICollectionView` festlegen zu der Klasse, die wir gerade erstellt haben:

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


## UI-Aktionen verbinden
<a name="getting-started-pub-sub-ios-actions"></a>

Wir sind nah dran, es gibt ein paar `IBActions` die wir erstellen müssen.

Zuerst kümmern wir uns um die „Beitreten“-Schaltfläche. Sie reagiert unterschiedlich je nach Wert von `connectingOrConnected`. Wenn sie bereits angeschlossen ist, verlässt sie einfach die Stage. Wenn die Verbindung unterbrochen ist, liest sie den Text aus dem Token `UITextField`und schafft eine neue `IVSStage` mit diesem Text. Dann fügen wir unseren `ViewController` hinzu als `strategy`, `errorDelegate`, und Renderer für `IVSStage`, und schließlich treten wir asynchron der Stage bei.

```
@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)")
        }
    }
}
```

Die andere UI-Aktion, die wir anschließen müssen, ist der Publish-Switch:

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

## Rendern der Teilnehmer
<a name="getting-started-pub-sub-ios-participants"></a>

Schließlich müssen wir die Daten, die wir vom SDK erhalten, in die Teilnehmerzelle rendern, die wir zuvor erstellt haben. Wir haben die Logik von `UICollectionView` bereits fertig, also müssen wir nur noch die API von `set` in `ParticipantCollectionViewCell.swift` implementieren.

Wir beginnen mit dem Hinzufügen der `empty`-Funktion und gehen Sie sie dann Schritt für Schritt durch:

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

Zuerst kümmern wir uns um den Status „Einfach“, die Teilnehmer-ID, den Veröffentlichungsstatus und den Abonnementstatus. Für diese aktualisieren wir einfach unsere `UILabels` direkt:

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

Die Texteigenschaften der Publish- und Subscribe-Enums stammen aus lokalen Erweiterungen:

```
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()
        }
    }
}
```

Als Nächstes aktualisieren wir die stummgeschalteten Audio- und Videozustände. Um den Stummschaltzustand zu erhalten, müssen wir den `IVSImageDevice` und `IVSAudioDevice` aus dem `streams`-Array finden. Um die Leistung zu optimieren, erinnern wir uns an die zuletzt angehängten Geräte-IDs.

```
// 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)"
```

Abschließend wollen wir eine Vorschau für das `imageDevice` rendern und  Audiostatistiken der `audioDevice` anzeigen:

```
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"
}
```

Die letzte Funktion, die wir erstellen müssen, ist `updatePreview()`, was unserer Ansicht eine Vorschau des Teilnehmers hinzufügt:

```
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)
        }
    }
}
```

Das Obige verwendet eine Hilfsfunktion auf `UIView`, um das Einbetten von Unteransichten zu vereinfachen:

```
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),
        ])
    }
}
```