

Terjemahan disediakan oleh mesin penerjemah. Jika konten terjemahan yang diberikan bertentangan dengan versi bahasa Inggris aslinya, utamakan versi bahasa Inggris.

# Memulai dengan IVS Real-Time Streaming
<a name="getting-started"></a>

Dokumen ini akan membawa Anda melalui langkah-langkah yang terlibat dalam mengintegrasikan Amazon IVS Real-Time Streaming ke dalam aplikasi Anda.

**Topics**
+ [Pengantar Streaming Waktu Nyata IVS](getting-started-introduction.md)
+ [Langkah 1: Mengatur Izin IAM](getting-started-iam-permissions.md)
+ [Langkah 2: Buat Panggung dengan Rekaman Peserta Opsional](getting-started-create-stage.md)
+ [Langkah 3: Bagikan Token Peserta](getting-started-distribute-tokens.md)
+ [Langkah 4: Integrasikan SDK Siaran IVS](getting-started-broadcast-sdk.md)
+ [Langkah 5: Publikasikan dan Berlangganan Video](getting-started-pub-sub.md)

# Pengantar Streaming Waktu Nyata IVS
<a name="getting-started-introduction"></a>

Bagian ini mencantumkan prasyarat untuk menggunakan streaming waktu nyata dan memperkenalkan terminologi utama.

## Prasyarat
<a name="getting-started-introduction-prereq"></a>

Sebelum Anda menggunakan Real-Time Streaming untuk pertama kalinya, selesaikan tugas-tugas berikut. Untuk petunjuk, lihat [Memulai Streaming Latensi Rendah IVS](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/getting-started.html).
+ Buat Akun AWS
+ Mengatur Root dan Pengguna Administratif

## Referensi Lainnya
<a name="getting-started-introduction-extref"></a>
+ [Referensi SDK Siaran Web IVS](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference)
+ [Referensi SDK Siaran Android IVS](https://aws.github.io/amazon-ivs-broadcast-docs/latest/android/)
+ [Referensi SDK Siaran IVS iOS](https://aws.github.io/amazon-ivs-broadcast-docs/latest/ios/)
+ [Referensi API Streaming Waktu Nyata IVS](https://docs.aws.amazon.com/ivs/latest/RealTimeAPIReference/Welcome.html)

## Terminologi Streaming Waktu Nyata
<a name="getting-started-introduction-terminology"></a>


| Istilah | Deskripsi | 
| --- | --- | 
| Stage | Ruang virtual tempat peserta dapat bertukar video secara real time. | 
| Host | Seorang peserta yang mengirim video lokal ke panggung. | 
| Pemirsa | Peserta yang menerima video dari tuan rumah. | 
| Peserta | Pengguna yang terhubung ke panggung sebagai host atau penampil. | 
| Token peserta | Token yang mengautentikasi peserta saat mereka bergabung dengan panggung. | 
| Siaran SDK | Pustaka klien yang memungkinkan peserta mengirim dan menerima video. | 

## Ikhtisar Langkah
<a name="getting-started-introduction-steps"></a>

1. [Siapkan Izin IAM](getting-started-iam-permissions.md) — Buat kebijakan AWS Identity and Access Management (IAM) yang memberi pengguna serangkaian izin dasar dan menetapkan kebijakan tersebut kepada pengguna.

1. [Buat panggung](getting-started-create-stage.md) — Buat ruang virtual tempat peserta dapat bertukar video secara real time.

1. [Mendistribusikan token peserta](getting-started-distribute-tokens.md) — Kirim token ke peserta sehingga mereka dapat bergabung dengan panggung Anda.

1. [Integrasikan SDK Siaran IVS](getting-started-broadcast-sdk.md) — Tambahkan SDK siaran ke aplikasi Anda untuk memungkinkan peserta mengirim dan menerima video:[Web](getting-started-broadcast-sdk.md#getting-started-broadcast-sdk-web),, [Android](getting-started-broadcast-sdk.md#getting-started-broadcast-sdk-android) dan. [iOS](getting-started-broadcast-sdk.md#getting-started-broadcast-sdk-ios)

1. [Publikasikan dan berlangganan video](getting-started-pub-sub.md) — Kirim video Anda ke panggung dan terima video dari host lain: [konsol IVS](getting-started-pub-sub.md#getting-started-pub-sub-console),, [Publikasikan & Berlangganan dengan IVS Web Broadcast SDK](getting-started-pub-sub-web.md)[Publikasikan & Berlangganan dengan IVS Android Broadcast SDK](getting-started-pub-sub-android.md), dan[Publikasikan & Berlangganan dengan IVS iOS Broadcast SDK](getting-started-pub-sub-ios.md).

# Langkah 1: Mengatur Izin IAM
<a name="getting-started-iam-permissions"></a>

Selanjutnya, Anda harus membuat kebijakan AWS Identity and Access Management (IAM) yang memberi pengguna serangkaian izin dasar (misalnya, untuk membuat tahap Amazon IVS dan membuat token peserta) dan menetapkan kebijakan tersebut kepada pengguna. [Anda dapat menetapkan izin saat membuat [pengguna baru atau menambahkan izin ke pengguna](#iam-permissions-new-user) yang sudah ada.](#iam-permissions-existing-user) Kedua prosedur diberikan di bawah ini.

Untuk informasi selengkapnya (misalnya, untuk mempelajari tentang pengguna dan kebijakan IAM, cara melampirkan kebijakan ke pengguna, dan cara membatasi apa yang dapat dilakukan pengguna dengan Amazon IVS), lihat:
+ [Membuat Pengguna IAM di Panduan Pengguna](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html#Using_CreateUser_console) *IAM*
+ Informasi di [Amazon IVS Security](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/security.html) tentang IAM dan “Kebijakan Terkelola untuk IVS.” 
+ Informasi IAM di [Amazon IVS](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/security.html) Security

Anda dapat menggunakan kebijakan terkelola AWS yang ada untuk Amazon IVS atau membuat kebijakan baru yang menyesuaikan izin yang ingin Anda berikan kepada sekumpulan pengguna, grup, atau peran. Kedua pendekatan tersebut dijelaskan di bawah ini.

## Menggunakan Kebijakan yang Ada untuk Izin IVS
<a name="iam-permissions-existing-policy"></a>

Dalam kebanyakan kasus, Anda akan ingin menggunakan kebijakan yang dikelola AWS untuk Amazon IVS. Mereka dijelaskan sepenuhnya di bagian [Kebijakan Terkelola untuk IVS](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/security-iam-awsmanpol.html) Keamanan *IVS*.
+ Gunakan kebijakan terkelola `IVSReadOnlyAccess` AWS untuk memberi pengembang aplikasi Anda akses ke semua operasi IVS Get dan List API (untuk streaming latensi rendah dan real-time).
+ Gunakan kebijakan terkelola `IVSFullAccess` AWS untuk memberi pengembang aplikasi Anda akses ke semua operasi API IVS (untuk streaming latensi rendah dan real-time).

## Opsional: Buat Kebijakan Kustom untuk Izin Amazon IVS
<a name="iam-permissions-new-policy"></a>

Ikuti langkah-langkah ini:

1. Masuk ke AWS Management Console dan buka konsol IAM di [https://console.aws.amazon.com/iam/](https://console.aws.amazon.com/iam/).

1. Pada panel navigasi, pilih **Kebijakan**, lalu pilih **Buat kebijakan**. Jendela **Tentukan izin** terbuka..

1. Di jendela **Tentukan izin**, pilih tab **JSON**, lalu salin dan tempel kebijakan IVS berikut ke area teks **editor kebijakan**. (Kebijakan ini tidak mencakup semua tindakan Amazon IVS. Anda dapat add/delete (Izinkan/Tolak) izin akses operasi sesuai kebutuhan. Lihat [Referensi API Streaming Waktu Nyata IVS](https://docs.aws.amazon.com//ivs/latest/RealTimeAPIReference/Welcome.html) untuk detail tentang operasi IVS.)

------
#### [ JSON ]

****  

   ```
   {
      "Version":"2012-10-17",		 	 	 
      "Statement": [
         {
            "Effect": "Allow",
            "Action": [
               "ivs:CreateStage",
               "ivs:CreateParticipantToken",
               "ivs:GetStage",
               "ivs:GetStageSession",
               "ivs:ListStages",
               "ivs:ListStageSessions",
               "ivs:CreateEncoderConfiguration",
               "ivs:GetEncoderConfiguration",
               "ivs:ListEncoderConfigurations",
               "ivs:GetComposition",
               "ivs:ListCompositions",
               "ivs:StartComposition",
               "ivs:StopComposition"
             ],
             "Resource": "*"
         },
         {
            "Effect": "Allow",
            "Action": [
               "cloudwatch:DescribeAlarms",
               "cloudwatch:GetMetricData",
               "s3:DeleteBucketPolicy",
               "s3:GetBucketLocation",
               "s3:GetBucketPolicy",
               "s3:PutBucketPolicy",
               "servicequotas:ListAWSDefaultServiceQuotas",
               "servicequotas:ListRequestedServiceQuotaChangeHistoryByQuota",
               "servicequotas:ListServiceQuotas",
               "servicequotas:ListServices",
               "servicequotas:ListTagsForResource"
            ],
            "Resource": "*"
         }
      ]
   }
   ```

------

1. Masih di jendela **Tentukan izin**, pilih **Berikutnya** (gulir ke bagian bawah jendela untuk melihat ini). Jendela **Review dan Create** terbuka. 

1. Pada jendela **Tinjau dan buat**, masukkan **nama Kebijakan** dan tambahkan **Deskripsi** secara opsional. Catat nama kebijakan, karena Anda akan membutuhkannya saat membuat pengguna (di bawah). Pilih **Buat kebijakan** (di bagian bawah jendela).

1. Anda dikembalikan ke jendela konsol IAM, di mana Anda akan melihat spanduk yang mengonfirmasi bahwa kebijakan baru Anda telah dibuat.

## Buat Pengguna Baru dan Tambahkan Izin
<a name="iam-permissions-new-user"></a>

### Kunci Akses Pengguna IAM
<a name="iam-permissions-new-user-access-keys"></a>

Kunci akses IAM terdiri dari ID kunci akses dan kunci akses rahasia. Mereka digunakan untuk menandatangani permintaan terprogram yang Anda buat ke AWS. Jika Anda tidak memiliki kunci akses, Anda dapat membuatnya dari AWS Management Console. Sebagai praktik terbaik, jangan membuat kunci akses root-user.

*Satu-satunya waktu Anda dapat melihat atau mengunduh kunci akses rahasia adalah ketika Anda membuat kunci akses. Anda tidak dapat memulihkannya nanti.* Namun, Anda dapat membuat kunci akses baru kapan saja; Anda harus memiliki izin untuk melakukan tindakan IAM yang diperlukan.

Selalu simpan kunci akses dengan aman. Jangan pernah membagikannya dengan pihak ketiga (bahkan jika pertanyaan tampaknya datang dari Amazon). Untuk informasi lebih lanjut, lihat [Mengelola access key untuk pengguna IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) dalam *Panduan Pengguna IAM*.

### Prosedur
<a name="iam-permissions-new-user-procedure"></a>

Ikuti langkah-langkah ini:

1. Di panel navigasi, pilih **Pengguna**, lalu pilih **Buat pengguna**. Jendela **Tentukan detail pengguna** terbuka. 

1. Di jendela **Tentukan detail pengguna**:

   1. Di bawah **Rincian pengguna**, ketik **nama Pengguna** baru yang akan dibuat.

   1. Periksa **Berikan akses pengguna ke AWS Management Console**.

   1. Di bawah **Kata sandi konsol**, pilih Kata **sandi yang dibuat otomatis**.

   1. Periksa **Pengguna harus membuat kata sandi baru saat masuk berikutnya**.

   1. Pilih **Berikutnya**. Jendela **Setel izin** terbuka.

1. Di bawah **Setel izin**, pilih **Lampirkan kebijakan secara langsung**. Jendela **kebijakan izin** terbuka.

1. Di kotak pencarian, masukkan nama kebijakan IVS (baik kebijakan terkelola AWS atau kebijakan kustom yang Anda buat sebelumnya). Ketika ditemukan, centang kotak untuk memilih kebijakan.

1. Pilih **Berikutnya** (di bagian bawah jendela). Jendela **Review dan Create** terbuka.

1. Pada jendela **Tinjau dan buat**, konfirmasikan bahwa semua detail pengguna sudah benar, lalu pilih **Buat pengguna** (di bagian bawah jendela).

1. Jendela **Ambil kata sandi** terbuka, berisi detail **login Konsol** Anda. *Simpan informasi ini dengan aman untuk referensi di masa mendatang*. Setelah selesai, pilih **Kembali ke daftar pengguna**.

## Menambahkan Izin ke Pengguna yang Ada
<a name="iam-permissions-existing-user"></a>

Ikuti langkah-langkah ini:

1. Masuk ke AWS Management Console dan buka konsol IAM di [https://console.aws.amazon.com/iam/](https://console.aws.amazon.com/iam/).

1. Di panel navigasi, pilih **Pengguna**, lalu pilih nama pengguna yang ada untuk diperbarui. (Pilih nama dengan mengkliknya; jangan centang kotak pilihan.)

1. Pada halaman **Ringkasan**, pada tab Izin, pilih **Tambahkan **izin****. Jendela **Tambahkan izin** terbuka.

1. Pilih **Lampirkan kebijakan yang ada secara langsung**. Jendela **kebijakan izin** terbuka.

1. Di kotak pencarian, masukkan nama kebijakan IVS (baik kebijakan terkelola AWS atau kebijakan kustom yang Anda buat sebelumnya). Ketika kebijakan ditemukan, centang kotak untuk memilih kebijakan.

1. Pilih **Berikutnya** (di bagian bawah jendela). Jendela **Review** terbuka.

1. Pada jendela **Review**, pilih **Add Permissions** (di bagian bawah jendela).

1. Pada halaman **Ringkasan**, konfirmasikan bahwa kebijakan IVS telah ditambahkan.

# Langkah 2: Buat Panggung dengan Rekaman Peserta Opsional
<a name="getting-started-create-stage"></a>

Panggung adalah ruang virtual di mana peserta dapat bertukar video secara real time. Ini adalah sumber daya dasar dari Real-Time Streaming API. Anda dapat membuat panggung menggunakan konsol atau CreateStage operasi.

Kami merekomendasikan bahwa jika memungkinkan, Anda membuat tahap baru untuk setiap sesi logis dan menghapusnya ketika selesai, daripada menjaga di sekitar tahapan lama untuk kemungkinan penggunaan kembali. Jika sumber daya basi (tahap lama, tidak untuk digunakan kembali) tidak dibersihkan, Anda cenderung mencapai batas jumlah maksimum tahapan lebih cepat.

Anda dapat membuat panggung - dengan atau tanpa rekaman peserta individu - melalui konsol Amazon IVS atau AWS CLI. Pembuatan dan perekaman panggung dibahas di bawah ini.

## Rekaman Peserta Individu
<a name="getting-started-create-stage-ipr-overview"></a>

Anda memiliki opsi untuk memungkinkan perekaman peserta individu untuk sebuah panggung. Jika perekaman peserta individu ke fitur S3 diaktifkan, semua siaran peserta individu ke panggung direkam dan disimpan ke bucket penyimpanan Amazon S3 yang Anda miliki. Selanjutnya, rekaman tersedia untuk pemutaran sesuai permintaan.

*Menyiapkan ini adalah opsi lanjutan.* Secara default, perekaman dinonaktifkan saat panggung dibuat.

Sebelum Anda dapat mengatur panggung untuk merekam, Anda harus membuat *konfigurasi penyimpanan*. Ini adalah sumber daya yang menentukan lokasi Amazon S3 tempat aliran yang direkam untuk panggung disimpan. Anda dapat membuat dan mengelola konfigurasi penyimpanan menggunakan konsol atau CLI; kedua prosedur diberikan di bawah ini. Setelah Anda membuat konfigurasi penyimpanan, Anda mengaitkannya dengan tahap baik saat Anda membuat panggung (seperti yang dijelaskan di bawah) atau yang lebih baru, dengan memperbarui tahap yang ada. (Di API, lihat [CreateStage](https://docs.aws.amazon.com//ivs/latest/RealTimeAPIReference/API_CreateStage.html)dan [UpdateStage](https://docs.aws.amazon.com//ivs/latest/RealTimeAPIReference/API_UpdateStage.html).) Anda dapat mengaitkan beberapa tahap dengan konfigurasi penyimpanan yang sama. Anda dapat menghapus konfigurasi penyimpanan yang tidak lagi terkait dengan tahapan apa pun.

Ingatlah kendala berikut:
+ Anda harus memiliki ember S3. Artinya, akun yang menyiapkan panggung untuk direkam harus memiliki bucket S3 tempat rekaman akan disimpan.
+ Panggung, konfigurasi penyimpanan, dan lokasi S3 harus berada di AWS wilayah yang sama. Jika Anda membuat tahapan di wilayah lain dan ingin merekamnya, Anda juga harus menyiapkan konfigurasi penyimpanan dan bucket S3 di wilayah tersebut.

Merekam ke bucket S3 Anda memerlukan otorisasi dengan kredensi AWS Anda. Untuk memberikan IVS akses yang diperlukan, AWS IAM [Service-Linked Role](https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html) (SLR) dibuat secara otomatis saat konfigurasi perekaman dibuat: SLR dibatasi untuk memberikan izin tulis IVS hanya pada bucket tertentu.

Perhatikan bahwa masalah jaringan antara lokasi streaming dan AWS atau di dalam AWS dapat mengakibatkan beberapa kehilangan data saat merekam streaming Anda. Dalam kasus ini, Amazon IVS memprioritaskan streaming langsung daripada rekaman. Untuk redundansi, rekam secara lokal melalui alat streaming Anda.

Untuk informasi selengkapnya (termasuk cara mengatur pasca-pemrosesan atau pemutaran VOD pada file yang direkam), lihat Perekaman [Peserta Individu](rt-individual-participant-recording.md).

### Cara Menonaktifkan Perekaman
<a name="getting-started-disable-recording"></a>

Untuk menonaktifkan perekaman Amazon S3 pada tahap yang ada:
+ **Konsol — Pada halaman detail untuk tahap yang relevan, di bagian **Rekam streaming peserta individu**, matikan **Aktifkan perekaman otomatis di bawah Rekam otomatis** **ke S3**, lalu pilih Simpan perubahan.** Ini menghapus asosiasi konfigurasi penyimpanan dengan panggung; aliran pada tahap itu tidak akan lagi direkam.
+ CLI — Jalankan `update-stage` perintah dan teruskan ARN konfigurasi perekaman sebagai string kosong:

  ```
  aws ivs-realtime update-stage --arn arn:aws:ivs:us-west-2:123456789012:stage/abcdABCDefgh --auto-participant-recording-configuration storageConfigurationArn=""
  ```

  Ini mengembalikan objek panggung dengan string kosong untuk`storageConfigurationArn`, menunjukkan bahwa rekaman dinonaktifkan.

## Petunjuk Konsol untuk Membuat Panggung IVS
<a name="getting-started-create-stage-console"></a>

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

   (Anda juga dapat mengakses konsol Amazon IVS melalui [Konsol Manajemen AWS](https://console.aws.amazon.com/).)

1. Di panel navigasi kiri, pilih **Tahapan**, lalu pilih **Buat tahap**. Jendela **Create stage** muncul.  
![\[Gunakan jendela Create stage untuk membuat tahap baru dan token peserta untuk itu.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Create_Stage_Console_IPR.png)

1. Secara opsional masukkan **nama Panggung**.

1. Jika Anda ingin mengaktifkan perekaman peserta individu, selesaikan langkah-langkah dalam [Mengatur Rekaman Peserta Individu Otomatis ke Amazon S3 (Opsional)](#getting-started-create-stage-ipr) di bawah ini.

1. Pilih **Buat tahap** untuk membuat panggung. Halaman detail panggung muncul, untuk tahap baru.

### Mengatur Rekaman Peserta Individu Otomatis ke Amazon S3 (Opsional)
<a name="getting-started-create-stage-ipr"></a>

Ikuti langkah-langkah ini untuk mengaktifkan perekaman peserta individu saat membuat panggung:

1. Pada halaman **Buat panggung**, di bawah **Rekam peserta individu**, aktifkan **Aktifkan perekaman otomatis**. Tampilan bidang tambahan, untuk memilih **jenis media yang direkam**, untuk memilih **konfigurasi Penyimpanan** yang ada atau membuat yang baru, dan untuk memilih apakah akan merekam thumbnail pada interval tertentu.  
![\[Gunakan dialog Rekam peserta individu untuk mengonfigurasi rekaman peserta individu untuk sebuah panggung.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Create_Stage_Console_enable_IPR.png)

1. Pilih jenis media mana yang akan direkam.

1. Pilih **Buat konfigurasi penyimpanan**. Jendela baru terbuka, dengan opsi untuk membuat ember Amazon S3 dan melampirkannya ke konfigurasi perekaman baru.  
![\[Gunakan jendela Buat konfigurasi penyimpanan untuk membuat konfigurasi penyimpanan baru untuk sebuah panggung.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Create_Storage_Configuration_IPR.png)

1. Isi kolom:

   1. Secara opsional masukkan **nama konfigurasi Penyimpanan**.

   1. Masukkan **nama Bucket**.

1. Pilih **Buat konfigurasi penyimpanan**, untuk membuat sumber daya konfigurasi penyimpanan baru dengan ARN unik. Biasanya, pembuatan konfigurasi perekaman membutuhkan beberapa detik, tetapi bisa sampai 20 detik. Ketika konfigurasi penyimpanan dibuat, Anda dikembalikan ke jendela **Create stage**. Di sana, area **Rekam peserta individu** menunjukkan **konfigurasi Penyimpanan** baru Anda dan bucket S3 (**Penyimpanan**) yang Anda buat.  
![\[Buat panggung menggunakan Konsol IVS: Konfigurasi penyimpanan baru dibuat.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Create_Stage_Console_Storage_Configuration.png)

1. Anda dapat secara opsional mengaktifkan opsi non-default lainnya seperti merekam replika peserta, menggabungkan rekaman peserta individu, dan perekaman thumbnail.  
![\[Buat panggung menggunakan Konsol IVS: aktifkan opsi lanjutan seperti perekaman thumbnail dan jahitan IPR.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Create_Stage_Console_IPR_Stitching.png)

## Petunjuk CLI untuk Membuat Tahap IVS
<a name="getting-started-create-stage-cli"></a>

Untuk menginstal AWS CLI, lihat [Menginstal atau memperbarui ke versi terbaru AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).

Sekarang Anda dapat menggunakan CLI untuk membuat dan mengelola sumber daya mengikuti salah satu dari dua prosedur di bawah ini, tergantung pada apakah Anda ingin membuat panggung dengan atau tanpa rekaman peserta individu diaktifkan.

### Buat Panggung tanpa Rekaman Peserta Individu
<a name="getting-started-create-stage-cli-without-ipr"></a>

API panggung berada di bawah namespace ivs-realtime. Misalnya, untuk membuat panggung:

```
aws ivs-realtime create-stage --name "test-stage"
```

Tanggapannya adalah:

```
{
   "stage": {
      "arn": "arn:aws:ivs:us-west-2:376666121854:stage/VSWjvX5XOkU3",
      "name": "test-stage"
   }
}
```

### Buat Panggung dengan Rekaman Peserta Individu
<a name="getting-started-create-stage-cli-with-ipr"></a>

Untuk membuat panggung dengan rekaman peserta individu diaktifkan:

```
aws ivs-realtime create-stage --name "test-stage-participant-recording" --auto-participant-recording-configuration storageConfigurationArn=arn:aws:ivs:us-west-2:123456789012:storage-configuration/LKZ6QR7r55c2,mediaTypes=AUDIO_VIDEO
```

Secara opsional, teruskan `thumbnailConfiguration` parameter untuk mengatur mode penyimpanan dan perekaman thumbnail secara manual, dan detik interval thumbnail:

```
aws ivs-realtime create-stage --name "test-stage-participant-recording" --auto-participant-recording-configuration storageConfigurationArn=arn:aws:ivs:us-west-2:123456789012:storage-configuration/LKZ6QR7r55c2,mediaTypes=AUDIO_VIDEO,thumbnailConfiguration="{targetIntervalSeconds=10,storage=[SEQUENTIAL,LATEST],recordingMode=INTERVAL}"
```

Secara opsional, teruskan `recordingReconnectWindowSeconds` parameter untuk mengaktifkan menggabungkan rekaman peserta individu yang terfragmentasi:

```
aws ivs-realtime create-stage --name "test-stage-participant-recording" --auto-participant-recording-configuration "storageConfigurationArn=arn:aws:ivs:us-west-2:123456789012:storage-configuration/LKZ6QR7r55c2,mediaTypes=AUDIO_VIDEO,thumbnailConfiguration="{targetIntervalSeconds=10,storage=[SEQUENTIAL,LATEST],recordingMode=INTERVAL}",recordingReconnectWindowSeconds=60"
```

Tanggapannya adalah:

```
{
   "stage": {
      "arn": "arn:aws:ivs:us-west-2:123456789012:stage/VSWjvX5XOkU3",
      "autoParticipantRecordingConfiguration": {
         "hlsConfiguration": {
             "targetSegmentDurationSeconds": 6
         },
         "mediaTypes": [
            "AUDIO_VIDEO"
         ],
         "recordingReconnectWindowSeconds": 60,
         "recordParticipantReplicas": true,
         "storageConfigurationArn": "arn:aws:ivs:us-west-2:123456789012:storage-configuration/LKZ6QR7r55c2",
         "thumbnailConfiguration": {
            "recordingMode": "INTERVAL",
            "storage": [
               "SEQUENTIAL",
               "LATEST"
            ],
            "targetIntervalSeconds": 10
         }
      },
      "endpoints": {
         "events": "<events-endpoint>",
         "rtmp": "<rtmp-endpoint>",
         "rtmps": "<rtmps-endpoint>",
         "whip": "<whip-endpoint>"
      },
      "name": "test-stage-participant-recording"
   }
}
```

# Langkah 3: Bagikan Token Peserta
<a name="getting-started-distribute-tokens"></a>

Sekarang setelah Anda memiliki panggung, Anda perlu membuat token dan mendistribusikannya kepada peserta, untuk memungkinkan peserta bergabung dengan panggung dan mulai mengirim dan menerima video. Ada dua pendekatan untuk menghasilkan token:
+ [Buat](#getting-started-distribute-tokens-self-signed) token dengan key pair.
+ [Buat token dengan real-time-streaming API IVS](#getting-started-distribute-tokens-api).

Kedua pendekatan ini dijelaskan di bawah ini.

## Membuat Token dengan Pasangan Kunci
<a name="getting-started-distribute-tokens-self-signed"></a>

Anda dapat membuat token pada aplikasi server Anda dan mendistribusikannya kepada peserta untuk bergabung dengan panggung. Anda perlu membuat public/private key pair ECDSA untuk menandatangani JWTs dan mengimpor kunci publik ke IVS. Kemudian IVS dapat memverifikasi token pada saat tahap bergabung. 

IVS tidak menawarkan kedaluwarsa kunci. Jika kunci pribadi Anda dikompromikan, Anda harus menghapus kunci publik lama.

### Buat Pasangan Kunci Baru
<a name="getting-started-distribute-tokens-self-signed-create-key-pair"></a>

Ada berbagai cara untuk membuat key pair. Di bawah ini, kami memberikan dua contoh.

Untuk membuat key pair baru di konsol, ikuti langkah-langkah berikut:

1. Buka [konsol Amazon IVS](https://console.aws.amazon.com/ivs). Pilih wilayah panggung Anda jika Anda belum berada di sana.

1. Di menu navigasi kiri, pilih **Streaming waktu nyata > Kunci publik**.

1. Pilih **Buat kunci publik**. Dialog **Create public key** muncul.

1. Ikuti petunjuknya dan pilih **Buat**.

1. Amazon IVS menghasilkan key pair baru. Kunci publik diimpor sebagai sumber daya kunci publik dan kunci pribadi segera tersedia untuk diunduh. Kunci publik juga dapat diunduh nanti jika perlu.

   Amazon IVS menghasilkan kunci di sisi klien dan tidak menyimpan kunci pribadi. ***Pastikan Anda menyimpan kuncinya; Anda tidak dapat mengambilnya nanti.***

[Untuk membuat key pair P384 EC baru dengan OpenSSL (Anda mungkin harus menginstal OpenSSL terlebih dahulu), ikuti langkah-langkah ini.](https://www.openssl.org/source/) Proses ini memungkinkan Anda untuk mengakses kunci pribadi dan publik. Anda memerlukan kunci publik hanya jika Anda ingin menguji verifikasi token Anda.

```
openssl ecparam -name secp384r1 -genkey -noout -out priv.pem
openssl ec -in priv.pem -pubout -out public.pem
```

Sekarang impor kunci publik baru Anda, menggunakan petunjuk di bawah ini.

### Impor Kunci Publik
<a name="getting-started-distribute-tokens-import-public-key"></a>

Setelah Anda memiliki key pair, Anda dapat mengimpor kunci publik ke IVS. Kunci pribadi tidak diperlukan oleh sistem kami tetapi digunakan oleh Anda untuk menandatangani token.

Untuk mengimpor kunci publik yang ada dengan konsol:

1. Buka [konsol Amazon IVS](https://console.aws.amazon.com/ivs). Pilih wilayah panggung Anda jika Anda belum berada di sana.

1. Di menu navigasi kiri, pilih **Streaming waktu nyata > Kunci publik**.

1. Pilih **Impor**. Dialog **kunci publik Impor** muncul.

1. Ikuti petunjuknya dan pilih **Impor**.

1. Amazon IVS mengimpor kunci publik Anda dan menghasilkan sumber daya kunci publik.

Untuk mengimpor kunci publik yang ada dengan CLI:

```
aws ivs-realtime import-public-key --public-key-material "`cat public.pem`" --region <aws-region>
```

Anda dapat menghilangkan `--region <aws-region>` jika wilayah tersebut ada di file konfigurasi AWS lokal Anda.

Berikut adalah contoh respons :

```
{
    "publicKey": {
        "arn": "arn:aws:ivs:us-west-2:123456789012:public-key/f99cde61-c2b0-4df3-8941-ca7d38acca1a",
        "fingerprint": "98:0d:1a:a0:19:96:1e:ea:0a:0a:2c:9a:42:19:2b:e7",
        "publicKeyMaterial": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVjYMV+P4ML6xemanCrtse/FDwsNnpYmS\nS6vRV9Wx37mjwi02hObKuCJqpj7x0lpz0bHm5v1JBvdZYAd/r2LR5aChK+/GM2Wj\nl8MG9NJIVFaw1u3bvjEjzTASSfS1BDX1\n-----END PUBLIC KEY-----\n",
        "tags": {}
    }
}
```

### Permintaan API
<a name="getting-started-distribute-tokens-create-api"></a>

```
POST /ImportPublicKey HTTP/1.1
{
  "publicKeyMaterial": "<pem file contents>"
}
```

### Menghasilkan dan Menandatangani Token
<a name="getting-started-distribute-tokens-self-signed-generate-sign"></a>

Untuk detail tentang bekerja dengan JWTs dan pustaka yang didukung untuk menandatangani token, kunjungi [jwt.io](https://jwt.io/). Pada antarmuka jwt.io, Anda harus memasukkan kunci pribadi Anda untuk menandatangani token. Kunci publik hanya diperlukan jika Anda ingin memverifikasi token.

Semua JWTs memiliki tiga bidang: header, payload, dan signature.

Skema JSON untuk header dan payload JWT dijelaskan di bawah ini. Atau Anda dapat menyalin sampel JSON dari konsol IVS. Untuk mendapatkan header dan payload JSON dari konsol IVS:

1. Buka [konsol Amazon IVS](https://console.aws.amazon.com/ivs). Pilih wilayah panggung Anda jika Anda belum berada di sana.

1. Di menu navigasi kiri, pilih **Streaming waktu nyata > Tahapan**.

1. Pilih tahap yang ingin Anda gunakan. Pilih **Lihat detail**.

1. Di bagian **Token peserta**, pilih drop-down di sebelah **Buat token**.

1. Pilih **Build token header dan payload**.

1. Isi formulir dan salin header JWT dan payload yang ditunjukkan di bagian bawah popup.

#### Skema Token: Header
<a name="getting-started-distribute-tokens-self-signed-generate-sign-header"></a>

Header menentukan:
+ `alg`adalah algoritma penandatanganan. Ini adalah ES384, algoritma tanda tangan ECDSA yang menggunakan algoritma hash SHA-384.
+ `typ`adalah jenis token, JWT.
+ `kid`adalah ARN dari kunci publik yang digunakan untuk menandatangani token. Itu harus ARN yang sama yang dikembalikan dari permintaan [ GetPublicKey](https://docs.aws.amazon.com//ivs/latest/RealTimeAPIReference/API_GetPublicKey.html)API.

```
{
  "alg": "ES384",
  "typ": "JWT"
  “kid”: “arn:aws:ivs:123456789012:us-east-1:public-key/abcdefg12345”
}
```

#### Skema Token: Muatan
<a name="getting-started-distribute-tokens-self-signed-generate-sign-payload"></a>

Muatan berisi data khusus untuk IVS. Semua bidang kecuali `user_id` wajib.
+ `RegisteredClaims`dalam spesifikasi JWT adalah klaim cadangan yang perlu disediakan agar token tahap valid: 
  + `exp`(waktu kedaluwarsa) adalah stempel waktu Unix UTC saat token kedaluwarsa. (Stempel waktu Unix adalah nilai numerik yang mewakili jumlah detik dari 1970-01-01T 00:00:00 Z UTC hingga tanggal/waktu UTC yang ditentukan, mengabaikan detik kabisat.) Token divalidasi saat peserta bergabung dengan panggung. IVS menyediakan token dengan TTL 12 jam default, yang kami rekomendasikan; ini dapat diperpanjang hingga maksimal 14 hari sejak dikeluarkan pada waktu (iat). Ini harus berupa nilai tipe integer.
  + `iat`(dikeluarkan pada waktu) adalah stempel waktu Unix UTC ketika JWT dikeluarkan. (Lihat catatan `exp` tentang stempel waktu Unix.) Ini harus berupa nilai tipe integer.
  + `jti`(JWT ID) adalah ID peserta yang digunakan untuk melacak dan merujuk pada peserta yang diberikan token. Setiap token harus memiliki ID peserta yang unik. Itu harus berupa string case-sensitive, hingga 64 karakter, hanya berisi karakter alfanumerik, tanda hubung (-), dan garis bawah (\$1). Tidak ada karakter khusus lainnya yang diizinkan. 
+ `user_id`adalah nama opsional yang ditetapkan pelanggan untuk membantu mengidentifikasi token; ini dapat digunakan untuk menautkan peserta ke pengguna dalam sistem pelanggan sendiri. Ini harus cocok dengan `userId` bidang dalam permintaan [CreateParticipantToken](https://docs.aws.amazon.com/ivs/latest/RealTimeAPIReference/API_CreateParticipantToken.html)API. Ini bisa berupa teks yang dikodekan UTF-8 dan merupakan string hingga 128 karakter. *Bidang ini diekspos ke semua peserta tahap dan tidak boleh digunakan untuk mengidentifikasi pribadi, rahasia, atau informasi sensitif.*
+ `resource`adalah ARN dari panggung; misalnya,. `arn:aws:ivs:us-east-1:123456789012:stage/oRmLNwuCeMlQ`
+ `topic`adalah ID panggung, yang dapat diekstraksi dari tahap ARN. Misalnya, jika tahap ARN adalah`arn:aws:ivs:us-east-1:123456789012:stage/oRmLNwuCeMlQ`, ID tahap adalah. `oRmLNwuCeMlQ`
+ `events_url`harus berupa titik akhir peristiwa yang dikembalikan dari GetStage operasi CreateStage atau. Kami menyarankan Anda menyimpan nilai ini pada waktu pembuatan tahap; nilainya dapat di-cache hingga 14 hari. Contoh nilai adalah `wss://global.events.live-video.net`.
+ `whip_url`harus menjadi titik akhir WHIP yang dikembalikan dari operasi CreateStage atau GetStage . Kami menyarankan Anda menyimpan nilai ini pada waktu pembuatan tahap; nilainya dapat di-cache hingga 14 hari. Contoh nilai adalah `https://453fdfd2ad24df.global-bm.whip.live-video.net`.
+ `capabilities`menentukan kemampuan token; nilai yang valid adalah `allow_publish` dan`allow_subscribe`. Untuk token khusus berlangganan, setel hanya ke. `allow_subscribe` `true`
+ `attributes`adalah bidang opsional tempat Anda dapat menentukan atribut yang disediakan aplikasi untuk dikodekan ke dalam token dan dilampirkan ke panggung. Kunci dan nilai peta dapat berisi teks yang dikodekan UTF-8. Panjang maksimum bidang ini adalah total 1 KB. *Bidang ini diekspos ke semua peserta tahap dan tidak boleh digunakan untuk mengidentifikasi pribadi, rahasia, atau informasi sensitif.*
+ `version`harus`1.0`.

  ```
  {
    "exp": 1697322063,
    "iat": 1697149263,
    "jti": "Mx6clRRHODPy",
    "user_id": "<optional_customer_assigned_name>",
    "resource": "<stage_arn>",
    "topic": "<stage_id>",
    "events_url": "wss://global.events.live-video.net",
    "whip_url": "https://114ddfabadaf.global-bm.whip.live-video.net",
    "capabilities": {
      "allow_publish": true,
      "allow_subscribe": true
    },
    "attributes": {
      "optional_field_1": "abcd1234",
      "optional_field_2": "false"
    },
    "version": "1.0"
  }
  ```

#### Skema Token: Tanda Tangan
<a name="getting-started-distribute-tokens-self-signed-generate-sign-signature"></a>

Untuk membuat tanda tangan, gunakan kunci pribadi dengan algoritme yang ditentukan di header (ES384) untuk menandatangani header yang dikodekan dan muatan yang dikodekan.

```
ECDSASHA384(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  <private-key>
)
```

#### Petunjuk
<a name="getting-started-distribute-tokens-self-signed-generate-sign-instructions"></a>

1. Hasilkan tanda tangan token dengan algoritme ES384 penandatanganan dan kunci pribadi yang terkait dengan kunci publik yang diberikan kepada IVS.

1. Merakit token.

   ```
   base64UrlEncode(header) + "." +
   base64UrlEncode(payload) + "." +
   base64UrlEncode(signature)
   ```

## Membuat Token dengan IVS Real-Time Streaming API
<a name="getting-started-distribute-tokens-api"></a>

![\[Mendistribusikan token peserta: Alur kerja token panggung\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Distribute_Participant_Tokens.png)


Seperti yang ditunjukkan di atas, aplikasi klien meminta aplikasi server Anda untuk token, dan aplikasi server memanggil CreateParticipantToken menggunakan permintaan yang ditandatangani AWS SDK atau SigV4. Karena AWS kredensil digunakan untuk memanggil API, token harus dibuat dalam aplikasi sisi server yang aman, bukan aplikasi sisi klien.

Saat membuat token peserta, Anda dapat menentukan atribut dan/atau kemampuan secara opsional:
+ Anda dapat menentukan atribut yang disediakan aplikasi untuk dikodekan ke dalam token dan melampirkan ke panggung. Kunci dan nilai peta dapat berisi teks yang dikodekan UTF-8. Panjang maksimum bidang ini adalah total 1 KB. *Bidang ini diekspos ke semua peserta tahap dan tidak boleh digunakan untuk mengidentifikasi pribadi, rahasia, atau informasi sensitif.*
+ Anda dapat menentukan kemampuan yang diaktifkan oleh token. Standarnya adalah `PUBLISH` dan`SUBSCRIBE`, yang memungkinkan peserta untuk mengirim dan menerima audio dan video, tetapi Anda dapat mengeluarkan token dengan subset kemampuan. Misalnya, Anda dapat mengeluarkan token hanya dengan `SUBSCRIBE` kemampuan moderator. Dalam hal ini, moderator dapat melihat peserta yang mengirim video tetapi tidak mengirim video mereka sendiri.

Lihat perinciannya di [CreateParticipantToken](https://docs.aws.amazon.com//ivs/latest/RealTimeAPIReference/API_CreateParticipantToken.html).

Anda dapat membuat token peserta melalui konsol atau CLI untuk pengujian dan pengembangan, tetapi kemungkinan besar Anda ingin membuatnya dengan AWS SDK di lingkungan produksi Anda.

Anda akan memerlukan cara untuk mendistribusikan token dari server Anda ke setiap klien (misalnya, melalui permintaan API). Kami tidak menyediakan fungsi ini. Untuk panduan ini, Anda cukup menyalin dan menempelkan token ke kode klien dalam langkah-langkah berikut.

**Penting**: Perlakukan token sebagai buram; yaitu, jangan membangun fungsionalitas berdasarkan konten token. Format token bisa berubah di masa depan.

### Instruksi Konsol
<a name="getting-started-distribute-tokens-console"></a>

1. Arahkan ke tahap yang Anda buat pada langkah sebelumnya.

1. Pilih **Buat token**. Jendela **Create token** muncul.

1. Masukkan ID pengguna untuk dikaitkan dengan token. Ini bisa berupa teks yang dikodekan UTF-8. 

1. Pilih **Buat**.

1. Salin token. *Penting: Pastikan untuk menyimpan token; IVS tidak menyimpannya dan Anda tidak dapat mengambilnya nanti*.

### Instruksi CLI
<a name="getting-started-distribute-tokens-cli"></a>

Membuat token dengan AWS CLI mengharuskan Anda mengunduh dan mengonfigurasi CLI terlebih dahulu di mesin Anda. Untuk detail, lihat [Panduan Pengguna AWS Command Line Interface](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html). Perhatikan bahwa membuat token dengan AWS CLI bagus untuk tujuan pengujian, tetapi untuk penggunaan produksi, kami menyarankan Anda membuat token di sisi server dengan AWS SDK (lihat petunjuk di bawah).

1. Jalankan `create-participant-token` perintah dengan tahap ARN. Sertakan salah satu atau semua kemampuan berikut:`"PUBLISH"`,`"SUBSCRIBE"`.

   ```
   aws ivs-realtime create-participant-token --stage-arn arn:aws:ivs:us-west-2:376666121854:stage/VSWjvX5XOkU3 --capabilities '["PUBLISH", "SUBSCRIBE"]'
   ```

1. Ini mengembalikan token peserta:

   ```
   {
       "participantToken": {
           "capabilities": [
               "PUBLISH",
               "SUBSCRIBE"
           ],
           "expirationTime": "2023-06-03T07:04:31+00:00",
           "participantId": "tU06DT5jCJeb",
           "token": "eyJhbGciOiJLTVMiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2NjE1NDE0MjAsImp0aSI6ImpGcFdtdmVFTm9sUyIsInJlc291cmNlIjoiYXJuOmF3czppdnM6dXMtd2VzdC0yOjM3NjY2NjEyMTg1NDpzdGFnZS9NbzhPUWJ0RGpSIiwiZXZlbnRzX3VybCI6IndzczovL3VzLXdlc3QtMi5ldmVudHMubGl2ZS12aWRlby5uZXQiLCJ3aGlwX3VybCI6Imh0dHBzOi8vNjZmNzY1YWM4Mzc3Lmdsb2JhbC53aGlwLmxpdmUtdmlkZW8ubmV0IiwiY2FwYWJpbGl0aWVzIjp7ImFsbG93X3B1Ymxpc2giOnRydWUsImFsbG93X3N1YnNjcmliZSI6dHJ1ZX19.MGQCMGm9affqE3B2MAb_DSpEm0XEv25hfNNhYn5Um4U37FTpmdc3QzQKTKGF90swHqVrDgIwcHHHIDY3c9eanHyQmcKskR1hobD0Q9QK_GQETMQS54S-TaKjllW9Qac6c5xBrdAk"
       }
   }
   ```

1. Simpan token ini. Anda akan membutuhkan ini untuk bergabung dengan panggung dan mengirim dan menerima video.

### AWS Instruksi SDK
<a name="getting-started-distribute-tokens-sdk"></a>

Anda dapat menggunakan AWS SDK untuk membuat token. Di bawah ini adalah petunjuk penggunaan AWS JavaScript SDK. 

**Penting:** Kode ini harus dijalankan di sisi server dan output-nya diberikan kepada klien.

**Prasyarat:** Untuk menggunakan contoh kode di bawah ini, Anda perlu menginstal paket aws-sdk/. client-ivs-realtime Untuk detailnya, lihat [Memulai AWS SDK for JavaScript](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-started.html).

```
import { IVSRealTimeClient, CreateParticipantTokenCommand } from "@aws-sdk/client-ivs-realtime";

const ivsRealtimeClient = new IVSRealTimeClient({ region: 'us-west-2' });
const stageArn = 'arn:aws:ivs:us-west-2:123456789012:stage/L210UYabcdef';
const createStageTokenRequest = new CreateParticipantTokenCommand({
  stageArn,
});
const response = await ivsRealtimeClient.send(createStageTokenRequest);
console.log('token', response.participantToken.token);
```

# Langkah 4: Integrasikan SDK Siaran IVS
<a name="getting-started-broadcast-sdk"></a>

IVS menyediakan SDK siaran untuk web, Android, dan iOS yang dapat Anda integrasikan ke dalam aplikasi Anda. SDK siaran digunakan untuk mengirim dan menerima video. Jika Anda telah [mengonfigurasi RTMP Ingest untuk tahap Anda](https://docs.aws.amazon.com/ivs/latest/RealTimeUserGuide/rt-stream-ingest.html), Anda dapat menggunakan encoder apa pun yang dapat disiarkan ke titik akhir RTMP (misalnya, OBS atau ffmpeg).

Pada bagian ini, kami menulis aplikasi sederhana yang memungkinkan dua atau lebih peserta untuk berinteraksi secara real time. Langkah-langkah di bawah ini memandu Anda melalui pembuatan aplikasi yang disebut BasicRealTime. Kode aplikasi lengkap aktif CodePen dan GitHub:
+  Situs web: [https://codepen. io/amazon-ivs/pen/ZEqgrpo](https://codepen.io/amazon-ivs/pen/ZEqgrpo) 
+  Android: [https://github.com/aws-samples/amazon-ivs-real-time- streaming-android-samples](https://github.com/aws-samples/amazon-ivs-real-time-streaming-android-samples) 
+  iOS: [https://github.com/aws-samples/amazon-ivs-real-time- streaming-ios-samples](https://github.com/aws-samples/amazon-ivs-real-time-streaming-ios-samples) 

## Web
<a name="getting-started-broadcast-sdk-web"></a>

### Mengatur File
<a name="getting-started-broadcast-sdk-web-setup"></a>

Untuk memulai, atur file Anda dengan membuat folder dan file HTML dan JS awal:

```
mkdir realtime-web-example
cd realtime-web-example
touch index.html
touch app.js
```

Anda dapat menginstal SDK siaran menggunakan tag skrip atau npm. Contoh kami menggunakan tag skrip untuk kesederhanaan tetapi mudah dimodifikasi jika Anda memilih untuk menggunakan npm nanti.

### Menggunakan Tag Script
<a name="getting-started-broadcast-sdk-web-script"></a>

SDK siaran Web didistribusikan sebagai JavaScript perpustakaan dan dapat diambil di [https://web-broadcast.live-video.net/1.33.0/ amazon-ivs-web-broadcast](https://web-broadcast.live-video.net/1.33.0/amazon-ivs-web-broadcast.js) .js.

Saat dimuat melalui `<script>` tag, pustaka mengekspos variabel global dalam lingkup jendela bernama`IVSBroadcastClient`.

### Menggunakan npm
<a name="getting-started-broadcast-sdk-web-npm"></a>

Untuk menginstal paket npm:

```
npm install amazon-ivs-web-broadcast
```

Anda sekarang dapat mengakses objek IVSBroadcast Klien:

```
const { Stage } = IVSBroadcastClient;
```

## Android
<a name="getting-started-broadcast-sdk-android"></a>

### Buat Proyek Android
<a name="getting-started-broadcast-sdk-android-project"></a>

1. Di Android Studio, buat **Proyek Baru**.

1. Pilih **Aktivitas Tampilan Kosong**.

   Catatan: Di beberapa versi Android Studio yang lebih lama, aktivitas berbasis Tampilan disebut **Empty Activity**. Jika jendela Android Studio menampilkan **Aktivitas Kosong** dan *tidak* menampilkan Aktivitas **Tampilan Kosong**, pilih **Aktivitas Kosong**. Jika tidak, jangan pilih **Aktivitas Kosong**, karena kita akan menggunakan View APIs (bukan Jetpack Compose).

1. Beri **nama** proyek Anda, lalu pilih **Selesai**.

### Instal Broadcast SDK
<a name="getting-started-broadcast-sdk-android-install"></a>

Untuk menambahkan library siaran Amazon IVS Android ke lingkungan pengembangan Android Anda, tambahkan pustaka ke `build.gradle` file modul Anda, seperti yang ditunjukkan di sini (untuk versi terbaru SDK siaran Amazon IVS). Dalam proyek yang lebih baru, `mavenCentral` repositori mungkin sudah disertakan dalam `settings.gradle` file Anda, jika itu masalahnya Anda dapat menghilangkan bloknya. `repositories` Untuk sampel kami, kami juga perlu mengaktifkan pengikatan data di `android` blok.

```
android {
    dataBinding.enabled true
}

repositories {
    mavenCentral()
}
 
dependencies {
     implementation 'com.amazonaws:ivs-broadcast:1.40.0:stages@aar'
}
```

Sebagai alternatif, untuk menginstal SDK secara manual, unduh versi terbaru dari lokasi ini:

[https://search.maven.org/artifact/com.amazonaws/ivs-broadcast](https://search.maven.org/artifact/com.amazonaws/ivs-broadcast)

## iOS
<a name="getting-started-broadcast-sdk-ios"></a>

### Buat Proyek iOS
<a name="getting-started-broadcast-sdk-ios-project"></a>

1. Buat proyek Xcode baru.

1. Untuk **Platform**, pilih **iOS**.

1. Untuk **Aplikasi**, pilih **Aplikasi**.

1. Masukkan **Nama Produk** aplikasi Anda, lalu pilih **Berikutnya**.

1. Pilih (navigasikan ke) direktori tempat menyimpan proyek, lalu pilih **Buat**.

Selanjutnya Anda perlu membawa SDK. Untuk petunjuknya, lihat [Menginstal Pustaka](broadcast-ios-getting-started.md#broadcast-ios-install) di *Panduan SDK Siaran iOS*.

### Konfigurasikan Izin
<a name="getting-started-broadcast-sdk-ios-config"></a>

Anda perlu memperbarui proyek Anda `Info.plist` untuk menambahkan dua entri baru untuk `NSCameraUsageDescription` dan`NSMicrophoneUsageDescription`. Untuk nilainya, berikan penjelasan yang dihadapi pengguna tentang mengapa aplikasi Anda meminta akses kamera dan mikrofon.

![\[Konfigurasikan izin iOS.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/iOS_Configure.png)


# Langkah 5: Publikasikan dan Berlangganan Video
<a name="getting-started-pub-sub"></a>

Anda dapat publish/subscribe (real-time) ke IVS dengan:
+ [Siaran IVS](https://docs.aws.amazon.com//ivs/latest/LowLatencyUserGuide/getting-started-set-up-streaming.html#broadcast-sdk) asli SDKs, yang mendukung WebRTC dan RTMPS. Kami merekomendasikan ini, terutama untuk skenario produksi. Lihat detail di bawah ini untuk [Web](getting-started-pub-sub-web.md), [Android](getting-started-pub-sub-android.md), dan [iOS](getting-started-pub-sub-ios.md).
+ Konsol Amazon IVS — Ini cocok untuk menguji aliran. Lihat di bawah ini.
+ Perangkat lunak streaming dan encoder perangkat keras lainnya - Anda dapat menggunakan encoder streaming apa pun yang mendukung protokol RTMP, RTMPS, atau WHIP. Lihat [Stream Ingest](rt-stream-ingest.md) untuk informasi lebih lanjut.

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

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

   (Anda juga dapat mengakses konsol Amazon IVS melalui [AWS Management Console](https://console.aws.amazon.com/).)

1. Di panel navigasi, pilih **Tahapan**. (Jika panel navigasi diciutkan, perluas dengan memilih ikon hamburger.)

1. Pilih tahap yang ingin Anda berlangganan atau terbitkan, untuk pergi ke halaman detailnya.

1. Untuk berlangganan: Jika panggung memiliki satu atau lebih penerbit, Anda dapat berlangganan dengan menekan tombol **Berlangganan**, di bawah tab **Berlangganan**. (Tab berada di bawah bagian **Konfigurasi Umum**.)

1. Untuk mempublikasikan:

   1. Pilih tab **Publikasikan**.

   1. Anda akan diminta untuk memberikan akses konsol IVS ke kamera dan mikrofon Anda; **Izinkan** izin tersebut.

   1. Di bagian bawah tab **Publish**, gunakan kotak dropdown untuk memilih perangkat input untuk mikrofon dan kamera.

   1. Untuk memulai penerbitan, pilih **Mulai penerbitan**.

   1. Untuk melihat konten yang dipublikasikan, kembali ke tab **Berlangganan**.

   1. Untuk berhenti menerbitkan, buka tab **Publish** dan tekan tombol **Stop publishing** di bagian bawah.

**Catatan**: Berlangganan dan penerbitan menghabiskan sumber daya, dan Anda akan dikenakan tarif per jam untuk waktu Anda terhubung ke panggung. Untuk mempelajari lebih lanjut, lihat [Streaming Waktu Nyata](https://aws.amazon.com/ivs/pricing/#Real-Time_Streaming) di halaman Harga IVS.

# Publikasikan & Berlangganan dengan IVS Web Broadcast SDK
<a name="getting-started-pub-sub-web"></a>

Bagian ini akan membawa Anda melalui langkah-langkah yang terlibat dalam penerbitan dan berlangganan ke panggung menggunakan aplikasi web Anda.

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

Pertama mari kita buat boilerplate HTML dan impor pustaka sebagai tag skrip:

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

## Terima Masukan Token dan Tambahkan Join/Leave Tombol
<a name="getting-started-pub-sub-web-join"></a>

Di sini kita mengisi tubuh dengan kontrol input kita. Ini mengambil sebagai masukan token, dan mereka mengatur tombol **Gabung** dan **Tinggalkan**. Biasanya aplikasi akan meminta token dari API aplikasi Anda, tetapi untuk contoh ini Anda akan menyalin dan menempelkan token ke input token.

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

## Tambahkan Elemen Kontainer Media
<a name="getting-started-pub-sub-web-media"></a>

Elemen-elemen ini akan memegang media untuk peserta lokal dan jarak jauh kami. Kita menambahkan tag script untuk memuat logika aplikasi kita didefinisikan dalam`app.js`.

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

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

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

Ini melengkapi halaman HTML dan Anda akan melihat ini saat memuat `index.html` di browser:

![\[Lihat Streaming Waktu Nyata di browser: Pengaturan HTML selesai.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/RT_Browser_View.png)


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

Mari kita beralih ke mendefinisikan isi `app.js` file kita. Mulailah dengan mengimpor semua properti yang diperlukan dari global SDK:

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

## Buat Variabel Aplikasi
<a name="getting-started-pub-sub-web-vars"></a>

Tetapkan variabel untuk menyimpan referensi ke elemen HTML tombol **Gabung** dan **Tinggalkan** kami dan status penyimpanan untuk aplikasi:

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

## Buat JoinSage 1: Tentukan Fungsi dan Validasi Input
<a name="getting-started-pub-sub-web-joinstage1"></a>

`joinStage`Fungsi mengambil token input, membuat koneksi ke panggung, dan mulai mempublikasikan video dan audio yang diambil dari`getUserMedia`.

Untuk memulai, kita mendefinisikan fungsi dan memvalidasi status dan masukan token. Kami akan menyempurnakan fungsi ini di beberapa bagian berikutnya.

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

## Buat JoinSage 2: Dapatkan Media untuk Publikasikan
<a name="getting-started-pub-sub-web-joinstage2"></a>

Berikut adalah media yang akan dipublikasikan ke panggung:

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

## Buat JoinSage 3: Tentukan Strategi Panggung dan Buat Panggung
<a name="getting-started-pub-sub-web-joinstage3"></a>

Strategi tahap ini adalah inti dari logika keputusan yang digunakan SDK untuk memutuskan apa yang akan dipublikasikan dan peserta mana yang akan berlangganan. Untuk informasi selengkapnya tentang tujuan fungsi, lihat [Strategi](web-publish-subscribe.md#web-publish-subscribe-concepts-strategy).

Strategi ini sederhana. Setelah bergabung dengan panggung, publikasikan streaming yang baru saja kami ambil dan berlangganan audio dan video setiap peserta jarak jauh:

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

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

## Buat JoinSage 4: Menangani Acara Panggung dan Media Render
<a name="getting-started-pub-sub-web-joinstage4"></a>

Tahapan memancarkan banyak peristiwa. Kita harus mendengarkan dan merender `STAGE_PARTICIPANT_STREAMS_ADDED` dan `STAGE_PARTICIPANT_LEFT` menghapus media ke dan dari halaman. [Serangkaian peristiwa yang lebih lengkap tercantum dalam Acara.](web-publish-subscribe.md#web-publish-subscribe-concepts-events)

Perhatikan bahwa kita membuat empat fungsi pembantu di sini untuk membantu kita dalam mengelola elemen DOM yang diperlukan:`setupParticipant`,, `teardownParticipant``createVideoEl`, dan`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;
}
```

## Buat JoinSage 5: Bergabunglah dengan Panggung
<a name="getting-started-pub-sub-web-joinstage5"></a>

Mari selesaikan `joinStage` fungsi kita dengan akhirnya bergabung dengan panggung\$1

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

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

Tentukan `leaveStage` fungsi yang akan dipanggil tombol tinggalkan.

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

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

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

Kami akan menambahkan satu fungsi terakhir ke `app.js` file kami. Fungsi ini dipanggil segera ketika halaman dimuat dan menetapkan event handler untuk bergabung dan meninggalkan panggung.

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

## Jalankan Aplikasi dan Berikan Token
<a name="getting-started-pub-sub-run-app"></a>

Pada titik ini Anda dapat berbagi halaman web secara lokal atau dengan orang lain, [membuka halaman](#getting-started-pub-sub-web-media), dan memasukkan token peserta dan bergabung dengan panggung.

## Apa selanjutnya?
<a name="getting-started-pub-sub-next"></a>

Untuk contoh lebih detail yang melibatkan npm, React, dan lainnya, lihat [IVS Broadcast SDK: Web Guide (Real-Time Streaming Guide)](broadcast-web.md).

# Publikasikan & Berlangganan dengan IVS Android Broadcast SDK
<a name="getting-started-pub-sub-android"></a>

Bagian ini akan membawa Anda melalui langkah-langkah yang terlibat dalam menerbitkan dan berlangganan ke panggung menggunakan aplikasi Android Anda.

## Buat Tampilan
<a name="getting-started-pub-sub-android-views"></a>

Kita mulai dengan membuat tata letak sederhana untuk aplikasi kita menggunakan `activity_main.xml` file yang dibuat secara otomatis. Layout berisi `EditText` to add a token, Join`Button`, a `TextView` to show the stage state, dan `CheckBox` to toggle publishing.

![\[Siapkan tata letak penerbitan untuk aplikasi Android Anda.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_Android_1.png)


Berikut adalah XHTML di belakang tampilan:

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

Kami mereferensikan beberapa string IDs di sini, jadi kami akan membuat seluruh `strings.xml` file kami sekarang:

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

Mari kita tautkan pandangan tersebut di XHTML ke: `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)
}
```

Sekarang kita membuat tampilan item untuk kita`RecyclerView`. Untuk melakukan ini, klik kanan `res/layout` direktori Anda dan pilih **New > Layout Resource File**. Beri nama file baru ini`item_stage_participant.xml`.

![\[Buat tampilan item untuk aplikasi Android Anda RecyclerView.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_Android_2.png)


Tata letak untuk item ini sederhana: berisi tampilan untuk merender aliran video peserta dan daftar label untuk menampilkan informasi tentang peserta:

![\[Buat tampilan item untuk aplikasi Android Anda RecyclerView - label.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_Android_3.png)


Berikut adalah XML-nya:

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

File XHTML ini mengembang kelas yang belum kita buat,. `ParticipantItem` Karena XHTML menyertakan namespace lengkap, pastikan untuk memperbarui file XHTML ini ke namespace Anda. Mari kita buat kelas ini dan atur tampilan, tetapi biarkan kosong untuk saat ini.

Buat class Kotlin baru,`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)
    }
}
```

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

Untuk menggunakan kamera dan mikrofon, Anda perlu meminta izin dari pengguna. Kami mengikuti alur izin standar untuk ini:

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

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

Aplikasi kami melacak peserta secara lokal di a `MainViewModel.kt` dan status akan dikomunikasikan kembali ke `MainActivity` menggunakan Kotlin. [StateFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/)

Buat class `MainViewModel` Kotlin baru:

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

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

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

}
```

Dalam `MainActivity.kt` kami mengelola model tampilan kami:

```
import androidx.activity.viewModels

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

Untuk menggunakan `AndroidViewModel` dan `ViewModel` ekstensi Kotlin ini, Anda harus menambahkan yang berikut ini ke `build.gradle` file modul Anda:

```
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 Adaptor
<a name="getting-started-pub-sub-android-app-state-recycler"></a>

Kami akan membuat `RecyclerView.Adapter` subclass sederhana untuk melacak peserta kami dan memperbarui acara `RecyclerView` di atas panggung kami. Tapi pertama-tama, kita membutuhkan kelas yang mewakili peserta. Buat class `StageParticipant` Kotlin baru:

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

Kita akan menggunakan kelas ini di `ParticipantAdapter` kelas yang akan kita buat selanjutnya. Kita mulai dengan mendefinisikan kelas dan membuat variabel untuk melacak peserta:

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

Kita juga harus mendefinisikan kita `RecyclerView.ViewHolder` sebelum menerapkan sisa penggantian:

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

Dengan menggunakan ini, kita dapat menerapkan `RecyclerView.Adapter` penggantian standar:

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

Akhirnya, kami menambahkan metode baru yang akan kami panggil dari kami `MainViewModel` ketika perubahan pada peserta dilakukan. Metode ini adalah operasi CRUD standar pada adaptor.

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

Kembali `MainViewModel` kita perlu membuat dan menahan referensi ke adaptor ini:

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

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

Kita juga perlu melacak beberapa status tahap di dalamnya`MainViewModel`. Mari kita definisikan properti itu sekarang:

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

Untuk melihat pratinjau Anda sendiri sebelum bergabung dengan panggung, kami segera membuat peserta lokal:

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

Kami ingin memastikan kami membersihkan sumber daya ini ketika kami `ViewModel` dibersihkan. Kami `onCleared()` segera mengganti, jadi kami tidak lupa untuk membersihkan sumber daya ini.

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

Sekarang kita mengisi `streams` properti lokal kita segera setelah izin diberikan, menerapkan `permissionsGranted` metode yang kita sebut sebelumnya:

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

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

Tiga [konsep](android-publish-subscribe.md#android-publish-subscribe-concepts) inti mendasari fungsionalitas real-time: panggung, strategi, dan penyaji. Tujuan desain adalah meminimalkan jumlah logika sisi klien yang diperlukan untuk membangun produk yang berfungsi.

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

`Stage.Strategy`Implementasi kami sederhana:

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

Untuk meringkas, kami menerbitkan berdasarkan `publishEnabled` keadaan internal kami, dan jika kami mempublikasikan, kami akan mempublikasikan aliran yang kami kumpulkan sebelumnya. Akhirnya untuk sampel ini, kami selalu berlangganan peserta lain, menerima audio dan video mereka.

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

`StageRenderer`Implementasinya juga cukup sederhana, meskipun mengingat jumlah fungsi yang berisi lebih banyak kode. Pendekatan umum dalam penyaji ini adalah memperbarui `ParticipantAdapter` saat SDK memberi tahu kami tentang perubahan pada peserta. Ada skenario tertentu di mana kami menangani peserta lokal secara berbeda, karena kami telah memutuskan untuk mengelolanya sendiri sehingga mereka dapat melihat pratinjau kamera mereka sebelum bergabung.

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

## Menerapkan Kustom RecyclerView LayoutManager
<a name="getting-started-pub-sub-android-layout"></a>

Menempatkan jumlah peserta yang berbeda bisa menjadi rumit. Anda ingin mereka mengambil seluruh bingkai tampilan induk tetapi Anda tidak ingin menangani setiap konfigurasi peserta secara independen. Untuk membuatnya mudah, kita akan berjalan melalui penerapan a`RecyclerView.LayoutManager`.

Buat kelas baru lainnya,`StageLayoutManager`, yang harus diperluas`GridLayoutManager`. Kelas ini dirancang untuk menghitung tata letak untuk setiap peserta berdasarkan jumlah peserta dalam row/column tata letak berbasis aliran. Setiap baris memiliki tinggi yang sama dengan yang lain, tetapi kolom dapat memiliki lebar yang berbeda per baris. Lihat komentar kode di atas `layouts` variabel untuk deskripsi tentang cara menyesuaikan perilaku ini.

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

Kembali `MainActivity.kt` kita perlu mengatur adaptor dan pengelola tata letak untuk`RecyclerView`:

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

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

Kami semakin dekat; hanya ada beberapa tindakan UI yang perlu kita hubungkan.

Pertama kita akan `MainActivity` mengamati `StateFlow` perubahan dari`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)
        }
    }
}
```

Selanjutnya kita menambahkan pendengar ke tombol Join dan kotak centang Publish kami:

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

Kedua fungsi panggilan di atas di kami`MainViewModel`, yang kami terapkan sekarang:

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

## Rendering Peserta
<a name="getting-started-pub-sub-android-participants"></a>

Akhirnya, kita perlu merender data yang kita terima dari SDK ke item peserta yang kita buat sebelumnya. Kami sudah menyelesaikan `RecyclerView` logika, jadi kami hanya perlu mengimplementasikan `bind` API di`ParticipantItem`.

Kita akan mulai dengan menambahkan fungsi kosong dan kemudian berjalan melalui langkah demi langkah:

```
fun bind(participant: StageParticipant) {

}
```

Pertama kita akan menangani status mudah, ID peserta, status publikasi, dan status berlangganan. Untuk ini, kami hanya memperbarui kami `TextViews` secara langsung:

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

Selanjutnya kita akan memperbarui status audio dan video yang diredam. Untuk mendapatkan status diredam, kita perlu menemukan `ImageDevice` dan `AudioDevice` dari array stream. Untuk mengoptimalkan kinerja, kami mengingat perangkat terakhir yang terpasang 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"
}
```

Akhirnya kami ingin membuat pratinjau untuk`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
```

Dan kami menampilkan statistik audio dari: `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
```

# Publikasikan & Berlangganan dengan IVS iOS Broadcast SDK
<a name="getting-started-pub-sub-ios"></a>

Bagian ini akan membawa Anda melalui langkah-langkah yang terlibat dalam penerbitan dan berlangganan ke panggung menggunakan aplikasi iOS Anda.

## Buat Tampilan
<a name="getting-started-pub-sub-ios-views"></a>

Kami mulai dengan menggunakan `ViewController.swift` file yang dibuat secara otomatis untuk mengimpor `AmazonIVSBroadcast` dan kemudian menambahkan beberapa `@IBOutlets` ke tautan:

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

Sekarang kita membuat pandangan itu dan menghubungkannya`Main.storyboard`. Berikut adalah struktur tampilan yang akan kita gunakan:

![\[Gunakan Main.storyboard untuk membuat tampilan iOS.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_iOS_1.png)


Untuk AutoLayout konfigurasi, kita perlu menyesuaikan tiga tampilan. Tampilan pertama adalah **Collection View Partisipan** (a`UICollectionView`). Terikat **Memimpin**, **Tertinggal**, dan **Bawah** ke **Area Aman**. Juga terikat **Atas** ke **Controls Container**.

![\[Kustomisasi Tampilan Koleksi iOS Tampilan Peserta.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_iOS_2.png)


Tampilan kedua adalah **Controls Container**. Terikat **Memimpin**, **Melintasi**, dan **Atas** ke **Area Aman**:

![\[Sesuaikan tampilan Kontainer Kontrol iOS.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_iOS_3.png)


Tampilan ketiga dan terakhir adalah **Vertical Stack View**. Bound **Top**, **Leading**, **Trailing**, dan **Bottom** ke **Superview**. Untuk penataan, atur spasi ke 8, bukan 0.

![\[Sesuaikan tampilan Tumpukan Vertikal iOS.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_iOS_4.png)


**UIStackTampilan** akan menangani tata letak tampilan yang tersisa. Untuk ketiga **UIStackTampilan**, gunakan **Fill** sebagai **Alignment** dan **Distribution**.

![\[Sesuaikan tampilan iOS yang tersisa dengan UIStack Tampilan.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_iOS_5.png)


Akhirnya, mari kita tautkan pandangan ini ke pandangan kita`ViewController`. Dari atas, petakan tampilan berikut:
+ **Bidang Teks Gabung** mengikat ke`textFieldToken`.
+ **Tombol Gabung** mengikat ke`buttonJoin`.
+ **Label State** mengikat ke`labelState`.
+ **Beralih Publikasikan** mengikat ke`switchPublish`.
+ **Tampilan Koleksi Peserta** mengikat`collectionViewParticipants`.

Gunakan juga waktu ini untuk menyetel item **Peserta Tampilan Koleksi** menjadi milik: `dataSource` `ViewController`

![\[Setel DataSource dari Collection View Peserta untuk aplikasi iOS.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_iOS_6.png)


Sekarang kita membuat `UICollectionViewCell` subclass untuk membuat peserta. Mulailah dengan membuat file **Cocoa Touch Class** baru:

![\[Buat UICollection ViewCell untuk merender peserta real-time iOS.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_iOS_7.png)


Beri nama `ParticipantUICollectionViewCell` dan jadikan subclass `UICollectionViewCell` di Swift. Kami mulai di file Swift lagi, membuat tautan `@IBOutlets` untuk kami:

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

Dalam file XIB terkait, buat hierarki tampilan ini:

![\[Buat hierarki tampilan iOS di file XIB terkait.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_iOS_8.png)


Untuk AutoLayout, kita akan memodifikasi tiga tampilan lagi. Tampilan pertama adalah **View Preview Container**. Atur **Trailing**, **Leading**, **Top**, dan **Bottom** ke **Sel Tampilan Koleksi Peserta**.

![\[Kustomisasi tampilan iOS View Preview Container.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_iOS_9.png)


Pandangan kedua adalah **View**. Atur **Leading** dan **Top** ke **Sel Tampilan Koleksi Peserta** dan ubah nilainya menjadi 4.

![\[Sesuaikan tampilan Tampilan iOS.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_iOS_10.png)


Tampilan ketiga adalah **Stack View**. Atur **Trailing**, **Leading**, **Top**, dan **Bottom** ke **Superview** dan ubah nilainya menjadi 4.

![\[Sesuaikan tampilan Tampilan Tumpukan iOS.\]](http://docs.aws.amazon.com/id_id/ivs/latest/RealTimeUserGuide/images/Publish_iOS_11.png)


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

Kembali ke kami`ViewController`, kami akan menonaktifkan pengatur waktu idle sistem untuk mencegah perangkat tidur saat aplikasi kami digunakan:

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

Selanjutnya kami meminta izin kamera dan mikrofon dari sistem:

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

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

Kita perlu mengkonfigurasi kita `collectionViewParticipants` dengan file tata letak yang kita buat sebelumnya:

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

Untuk mewakili setiap peserta, kita membuat struct sederhana yang disebut`StageParticipant`. Ini dapat dimasukkan dalam `ViewController.swift` file, atau file baru dapat dibuat.

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

Untuk melacak peserta tersebut, kami menyimpan sederet dari mereka sebagai milik pribadi di`ViewController`:

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

Properti ini akan digunakan untuk memberi daya pada kami `UICollectionViewDataSource` yang ditautkan dari storyboard sebelumnya:

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

}
```

Untuk melihat pratinjau Anda sendiri sebelum bergabung dengan panggung, kami segera membuat peserta lokal:

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

Ini menghasilkan sel peserta segera dirender setelah aplikasi berjalan, mewakili peserta lokal.

Pengguna ingin dapat melihat diri mereka sendiri sebelum bergabung dengan tahap, jadi selanjutnya kita menerapkan `setupLocalUser()` metode yang dipanggil dari kode penanganan izin sebelumnya. Kami menyimpan referensi kamera dan mikrofon sebagai `IVSLocalStageStream` objek.

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

Di sini kami telah menemukan kamera dan mikrofon perangkat melalui SDK dan menyimpannya di `streams` objek lokal kami, kemudian menetapkan `streams` array peserta pertama (peserta lokal yang kami buat sebelumnya) ke kami`streams`. Akhirnya kami memanggil `participantsChanged` dengan `index` 0 dan `changeType` dari`updated`. Fungsi itu adalah fungsi pembantu untuk memperbarui kami `UICollectionView` dengan animasi yang bagus. Begini tampilannya:

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

Jangan khawatir`cell.set`; kita akan membahasnya nanti, tapi di situlah kita akan merender konten sel berdasarkan peserta.

`ChangeType`Ini adalah enum sederhana:

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

Akhirnya, kami ingin melacak apakah panggung terhubung. Kami menggunakan sederhana `bool` untuk melacaknya, yang secara otomatis akan memperbarui UI kami ketika diperbarui sendiri.

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

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

Tiga [konsep](ios-publish-subscribe.md#ios-publish-subscribe-concepts) inti mendasari fungsionalitas real-time: panggung, strategi, dan penyaji. Tujuan desain adalah meminimalkan jumlah logika sisi klien yang diperlukan untuk membangun produk yang berfungsi.

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

`IVSStageStrategy`Implementasi kami sederhana:

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

Untuk meringkas, kami hanya mempublikasikan jika sakelar publikasi berada di posisi “on”, dan jika kami mempublikasikan, kami akan mempublikasikan aliran yang kami kumpulkan sebelumnya. Akhirnya, untuk sampel ini, kami selalu berlangganan peserta lain, menerima audio dan video mereka.

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

`IVSStageRenderer`Implementasinya juga cukup sederhana, meskipun mengingat jumlah fungsi yang berisi lebih banyak kode. Pendekatan umum dalam perender ini adalah memperbarui `participants` array kami saat SDK memberi tahu kami tentang perubahan pada peserta. Ada skenario tertentu di mana kami menangani peserta lokal secara berbeda, karena kami telah memutuskan untuk mengelolanya sendiri sehingga mereka dapat melihat pratinjau kamera mereka sebelum bergabung.

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

Kode ini menggunakan ekstensi untuk mengubah status koneksi menjadi teks ramah manusia:

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

## Menerapkan Kustom UICollection ViewLayout
<a name="getting-started-pub-sub-ios-layout"></a>

Menempatkan jumlah peserta yang berbeda bisa menjadi rumit. Anda ingin mereka mengambil seluruh bingkai tampilan induk tetapi Anda tidak ingin menangani setiap konfigurasi peserta secara independen. Untuk membuatnya mudah, kita akan berjalan melalui penerapan a`UICollectionViewLayout`.

Buat file baru lainnya,`ParticipantCollectionViewLayout.swift`, yang harus diperluas`UICollectionViewLayout`. Kelas ini akan menggunakan kelas lain yang disebut`StageLayoutCalculator`, yang akan kita bahas segera. Kelas menerima nilai bingkai yang dihitung untuk setiap peserta dan kemudian menghasilkan `UICollectionViewLayoutAttributes` objek yang diperlukan.

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

Yang lebih penting adalah `StageLayoutCalculator.swift` kelas. Ini dirancang untuk menghitung frame untuk setiap peserta berdasarkan jumlah peserta dalam row/column tata letak berbasis aliran. Setiap baris memiliki tinggi yang sama dengan yang lain, tetapi kolom dapat memiliki lebar yang berbeda per baris. Lihat komentar kode di atas `layouts` variabel untuk deskripsi tentang cara menyesuaikan perilaku ini.

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

}
```

Kembali`Main.storyboard`, pastikan untuk mengatur kelas tata letak untuk kelas yang baru saja kita buat: `UICollectionView`

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


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

Kami semakin dekat, ada beberapa `IBActions` yang perlu kami buat.

Pertama kita akan menangani tombol join. Ini merespons secara berbeda berdasarkan nilai. `connectingOrConnected` Ketika sudah terhubung, itu hanya meninggalkan panggung. Jika terputus, ia membaca teks dari token `UITextField` dan membuat yang baru `IVSStage` dengan teks itu. Kemudian kita menambahkan kita `ViewController` sebagai`strategy`,`errorDelegate`, dan renderer untuk`IVSStage`, dan akhirnya kita bergabung dengan panggung secara asinkron.

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

Tindakan UI lain yang perlu kita hubungkan adalah sakelar publikasikan:

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

## Rendering Peserta
<a name="getting-started-pub-sub-ios-participants"></a>

Akhirnya, kita perlu merender data yang kita terima dari SDK ke sel peserta yang kita buat sebelumnya. Kami sudah menyelesaikan `UICollectionView` logika, jadi kami hanya perlu mengimplementasikan `set` API di`ParticipantCollectionViewCell.swift`.

Kita akan mulai dengan menambahkan `empty` fungsi dan kemudian berjalan melalui langkah demi langkah:

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

Pertama kita menangani status mudah, ID peserta, status publikasi, dan status berlangganan. Untuk ini, kami hanya memperbarui kami `UILabels` secara langsung:

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

Properti teks dari mempublikasikan dan berlangganan enum berasal dari ekstensi lokal:

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

Selanjutnya kami memperbarui status audio dan video yang diredam. Untuk mendapatkan status yang diredam kita perlu menemukan `IVSImageDevice` dan `IVSAudioDevice` dari `streams` array. Untuk mengoptimalkan kinerja, kami akan mengingat perangkat terakhir yang terpasang.

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

Akhirnya kami ingin membuat pratinjau untuk `imageDevice` dan menampilkan statistik audio dari`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"
}
```

Fungsi terakhir yang perlu kita buat adalah`updatePreview()`, yang menambahkan pratinjau peserta ke tampilan kita:

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

Di atas menggunakan fungsi pembantu `UIView` untuk mempermudah penyematan subview:

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