

# Introducción a transmisión en tiempo real de IVS
<a name="getting-started"></a>

Este documento explica los pasos necesarios para integrar la transmisión en tiempo real de Amazon IVS en su aplicación.

**Topics**
+ [Introducción a la transmisión en tiempo real de IVS](getting-started-introduction.md)
+ [Paso 1: configurar permisos de IAM](getting-started-iam-permissions.md)
+ [Paso 2: crear una fase con grabación de participantes opcional](getting-started-create-stage.md)
+ [Paso 3: distribuir los tokens de participantes](getting-started-distribute-tokens.md)
+ [Paso 4: integrar el SDK de transmisión de IVS](getting-started-broadcast-sdk.md)
+ [Paso 5: publicar y suscribirse a videos](getting-started-pub-sub.md)

# Introducción a la transmisión en tiempo real de IVS
<a name="getting-started-introduction"></a>

En esta sección se enumeran los requisitos previos para utilizar la transmisión en tiempo real y se presenta terminología clave.

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

Antes de usar la transmisión en tiempo real por primera vez, lleve a cabo las siguientes tareas: Para obtener instrucciones, consulte [Introducción a transmisión de baja latencia de IVS](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/getting-started.html).
+ Cree una cuenta de AWS
+ Configuración de usuarios raíz y administrativos

## Otras referencias
<a name="getting-started-introduction-extref"></a>
+ [Referencia del SDK de transmisión web de IVS](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference)
+ [Referencia del SDK de transmisión para Android de IVS](https://aws.github.io/amazon-ivs-broadcast-docs/latest/android/)
+ [Referencia del SDK de transmisión para iOS de IVS](https://aws.github.io/amazon-ivs-broadcast-docs/latest/ios/)
+ [Referencia de la API de transmisión en tiempo real de IVS](https://docs.aws.amazon.com/ivs/latest/RealTimeAPIReference/Welcome.html)

## Terminología de transmisión en tiempo real
<a name="getting-started-introduction-terminology"></a>


| Plazo | Descripción | 
| --- | --- | 
| Etapa | Un espacio virtual donde los participantes pueden intercambiar videos en tiempo real. | 
| Host | Un participante que envía un video de un entorno local al escenario. | 
| Visor | Un participante que recibe un video de los hosts. | 
| Participante | Un usuario conectado al escenario como host o espectador. | 
| Token de participante | Un token que autentica a un participante cuando se une a un escenario. | 
| SDK de transmisión | Una biblioteca cliente que permite a los participantes enviar y recibir videos. | 

## Información general sobre los pasos
<a name="getting-started-introduction-steps"></a>

1. [Configuración de permisos de IAM](getting-started-iam-permissions.md): cree una política de AWS Identity and Access Management (IAM) que proporcione a los usuarios un conjunto básico de permisos y asigne esa política a los usuarios.

1. [Creación de un escenario](getting-started-create-stage.md): cree un espacio virtual donde los participantes pueden intercambiar videos en tiempo real.

1. [Distribución de los tokens de participante](getting-started-distribute-tokens.md): envíe tokens a los participantes para que puedan unirse a su escenario.

1. [Integración del SDK de transmisión de IVS](getting-started-broadcast-sdk.md): agregue el SDK de transmisión a su aplicación para permitir a los participantes enviar y recibir videos: [Web](getting-started-broadcast-sdk.md#getting-started-broadcast-sdk-web), [Android](getting-started-broadcast-sdk.md#getting-started-broadcast-sdk-android) y [iOS](getting-started-broadcast-sdk.md#getting-started-broadcast-sdk-ios).

1. [Publicación de videos y suscripción para recibirlos](getting-started-pub-sub.md): envíe su video a la fase y reciba videos de otros hosts: [consola de IVS](getting-started-pub-sub.md#getting-started-pub-sub-console), [Publicación y suscripción con el SDK de transmisión para web de IVS](getting-started-pub-sub-web.md), [Publicación y suscripción con el SDK de transmisión para Android de IVS](getting-started-pub-sub-android.md) y [Publicación y suscripción con el SDK de transmisión para iOS de IVS](getting-started-pub-sub-ios.md).

# Paso 1: configurar permisos de IAM
<a name="getting-started-iam-permissions"></a>

A continuación, debe crear una política de AWS Identity and Access Management (IAM) que proporcione a los usuarios una serie de permisos básicos (por ejemplo, para crear un escenario Amazon IVS y crear tokens del participante), y asignar dicha política a los usuarios. Puede asignar los permisos cuando crea un [usuario nuevo](#iam-permissions-new-user) o agregarlos a un [usuario actual](#iam-permissions-existing-user). A continuación, se explican ambos procedimientos.

Para obtener más información (por ejemplo, para obtener información sobre los usuarios y las políticas de IAM, cómo adjuntar una política a un usuario y cómo restringir lo que los usuarios pueden hacer con Amazon IVS), consulte:
+ [Crear un usuario de IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html#Using_CreateUser_console) en la *Guía del usuario de IAM*.
+ La información de [Seguridad de Amazon IVS](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/security.html) sobre IAM y “Managed Policies for IVS”. 
+ La información de IAM en [Seguridad en Amazon IVS](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/security.html)

Puede utilizar una política administrada de AWS existente para Amazon IVS o crear una nueva que personalice los permisos que quiera conceder a un conjunto de usuarios, grupos o roles. A continuación se describen ambos enfoques.

## Uso de una política existente para los permisos de IVS
<a name="iam-permissions-existing-policy"></a>

En la mayoría de los casos, querrá utilizar una política administrada de AWS para Amazon IVS. Se describen detalladamente en la sección [Managed Policies for IVS](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/security-iam-awsmanpol.html) de *Seguridad de IVS*.
+ Utilice la política administrada de AWS `IVSReadOnlyAccess` para ofrecer a los desarrolladores de aplicaciones acceso a todas las operaciones de las API Get y List de IVS (tanto para transmisión de baja latencia como en tiempo real).
+ Utilice la política administrada de AWS `IVSFullAccess` para ofrecer a los desarrolladores de aplicaciones acceso a todas las operaciones de la API de IVS (tanto para transmisión de baja latencia como en tiempo real).

## Opcional: crear una política personalizada para los permisos de Amazon IVS
<a name="iam-permissions-new-policy"></a>

Siga estos pasos:

1. Inicie sesión en la consola de administración de AWS y abra la consola de IAM en [https://console.aws.amazon.com/iam/](https://console.aws.amazon.com/iam/).

1. En el panel de navegación, seleccione **Políticas** y, a continuación, elija **Crear política**. Se abre la ventana **Especificar permisos**.

1. En la ventana **Especificar permisos**, elija la pestaña **JSON**. Luego, copie y pegue la siguiente política de IVS en el área de texto del **Editor de políticas**. (La política no incluye todas las acciones de Amazon IVS. Puede agregar o eliminar, es decir, permitir o denegar, los permisos de acceso a la operación según sea necesario. Consulte la [Referencia de la API de transmisión en tiempo real de IVS](https://docs.aws.amazon.com//ivs/latest/RealTimeAPIReference/Welcome.html) para obtener más información sobre las operaciones de 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. En la ventana **Especificar permisos**, elija **Siguiente** (desplácese a la parte inferior de la ventana para verlo). Se abre la ventana **Revisar y crear**. 

1. En la ventana **Revisar y crear**, asígnele un **nombre** a la política y, si lo desea, agregue una **descripción**. Anote el nombre de la política, ya que lo necesitará cuando cree usuarios (más adelante). Elija **Create policy** (Crear política) (en la parte inferior de la ventana).

1. Volverá a la ventana de la consola de IAM, donde debería ver un banner que confirma la creación de la política nueva.

## Crear usuarios nuevos y agregar permisos
<a name="iam-permissions-new-user"></a>

### Claves de acceso de usuario de IAM
<a name="iam-permissions-new-user-access-keys"></a>

Las claves de acceso de IAM constan de un ID de clave de acceso y de una clave de acceso secreta. Se utilizan para firmar las solicitudes programáticas que realiza a AWS. Si no tiene claves de acceso, puede crearlas mediante la consola de administración de AWS. Como práctica recomendada, no cree claves de acceso del usuario raíz.

*El único momento que puede ver o descargar la clave de acceso secreta es cuando crea las claves de acceso. No puede recuperarla más adelante.* Sin embargo, puede crear claves de acceso nuevas en cualquier momento; debe tener permisos para realizar las acciones de IAM requeridas.

Siempre almacene las claves de acceso de forma segura. Nunca las comparta con terceros (incluso si parece que la consulta proviene de Amazon). Para obtener más información, consulte [Administración de claves de acceso para usuarios de IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) en la *Guía del usuario de IAM*.

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

Siga estos pasos:

1. En el panel de navegación, elija **Usuarios** y la opción **Agregar usuario**. Se abre la ventana **Especificar los detalles del usuario**. 

1. En la ventana **Especificar los detalles del usuario**:

   1. En **Detalles del usuario**, escriba el nuevo **nombre de usuario** que se va a crear.

   1. Active la casilla de verificación **Acceso de usuario a la consola de administración de AWS**.

   1. En **Contraseña de la consola**, seleccione **Contraseña generada de manera automática**.

   1. Seleccione la casilla de verificación **El usuario debe crear una contraseña nueva en el siguiente inicio de sesión**.

   1. Elija **Siguiente**. Se abre la ventana **Establecer permisos**.

1. En **Establecer permisos**, elija **Asociar directamente las políticas existentes**. Se abre la ventana **Políticas de permisos**.

1. En el cuadro de búsqueda, ingrese el nombre de una política de IVS (ya sea una política administrada de AWS o una política personalizada creada con anterioridad). Cuando la encuentre, marque la casilla para seleccionar la política.

1. Elija **Siguiente** (en la parte inferior de la ventana). Se abre la ventana **Revisar y crear**.

1. En la ventana **Revisar y crear**, confirme que toda la información del usuario sea correcta y, a continuación, elija **Crear usuario** (en la parte inferior de la ventana).

1. Se abre la ventana **Recuperar la contraseña**, que contiene los **detalles de inicio de sesión de la consola**. *Guarde esta información de forma segura para consultarla en el futuro*. Cuando haya terminado, elija **Volver a la lista de usuarios**.

## Agregar permisos para un usuario existente
<a name="iam-permissions-existing-user"></a>

Siga estos pasos:

1. Inicie sesión en la consola de administración de AWS y abra la consola de IAM en [https://console.aws.amazon.com/iam/](https://console.aws.amazon.com/iam/).

1. En el panel de navegación, elija **Users (Usuarios)** y, a continuación, elija un nombre de usuario existente para actualizarlo. (Haga clic en el nombre para elegirlo; no marque la casilla de selección.)

1. En la página **Resumen**, en la pestaña **Permisos**, elija **Agregar permisos**. Se abre la ventana **Agregar permisos**.

1. Seleccione **Asociar directamente las políticas existentes**. Se abre la ventana **Políticas de permisos**.

1. En el cuadro de búsqueda, ingrese el nombre de una política de IVS (ya sea una política administrada de AWS o una política personalizada creada con anterioridad). Cuando encuentre la política, marque la casilla para seleccionarla.

1. Elija **Siguiente** (en la parte inferior de la ventana). Se abre la ventana **Revisión**.

1. En la ventana de **Revisión**, selecciona **Agregar permisos** (en la parte inferior de la ventana).

1. En la página **Summary** (Resumen), confirme que se agregó la política de IVS.

# Paso 2: crear una fase con grabación de participantes opcional
<a name="getting-started-create-stage"></a>

Un escenario es un espacio virtual donde los participantes pueden intercambiar videos en tiempo real. Es el recurso fundamental de la API de transmisión en tiempo real. Puede crear una fase mediante la consola o a través de la operación CreateStage.

Le recomendamos que, siempre que sea posible, cree un nuevo escenario para cada sesión lógica y la elimine cuando termine, en lugar de conservar los escenarios antiguos para su posible reutilización. Si los recursos obsoletos (escenarios antiguos que no se van a reutilizar) no se eliminan, es probable que alcance el límite de escenarios más rápido.

Puede crear una fase (con o sin grabación de participantes individuales) a través de la consola de Amazon IVS o la AWS CLI. La creación y grabación de fases se analizan a continuación.

## Grabación de participantes individuales
<a name="getting-started-create-stage-ipr-overview"></a>

Tiene la opción de habilitar la grabación de participantes individuales en una fase. Si la característica de grabación de participantes individuales en S3 está habilitada, todas las transmisiones de participantes individuales en la fase se graban y se guardan en un bucket de almacenamiento de Amazon S3 de su propiedad. Posteriormente, la grabación estará disponible para la reproducción bajo demanda.

*Esta configuración es una opción avanzada.* De forma predeterminada, la grabación está deshabilitada cuando se crea una fase.

Para poder configurar una fase para la grabación, debe crear una *configuración de almacenamiento*. Se trata de un recurso que especifica una ubicación de Amazon S3 en la que se almacenan las transmisiones grabadas para la fase. Puede crear y administrar configuraciones de almacenamiento mediante la consola o la CLI; ambos procedimientos se detallan a continuación. Después de crear la configuración de almacenamiento, la asociará con una fase cuando cree una nueva fase (como se describe a continuación) o posteriormente al actualizar una fase existente. (En la API, consulte [CreateStage](https://docs.aws.amazon.com//ivs/latest/RealTimeAPIReference/API_CreateStage.html) y [UpdateStage](https://docs.aws.amazon.com//ivs/latest/RealTimeAPIReference/API_UpdateStage.html)). Puede asociar varias fases con la misma configuración de almacenamiento. Puede eliminar una configuración de almacenamiento que ya no esté asociada a ninguna fase.

Tenga en cuenta las siguientes consideraciones:
+ Debe poseer el bucket de S3. Es decir, la cuenta que configura una fase para ser grabada debe poseer el bucket de S3 donde se almacenarán las grabaciones.
+ La fase, la configuración del almacenamiento y la ubicación de S3 deben estar en la misma región de AWS. Si crea fases en otras regiones y desea grabarlas, también debe establecer configuraciones de almacenamiento y buckets de S3 en esas regiones.

La grabación en su bucket de S3 requiere autorización con sus credenciales de AWS. Para proporcionar a IVS el acceso necesario, un [rol vinculado al servicio](https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html) (SLR) de AWS IAM se crea automáticamente cuando se crea la configuración de grabación: el SLR está limitado a conceder permiso de escritura de IVS solo en el bucket específico.

Tenga en cuenta que los problemas de red entre la ubicación del streaming y AWS o dentro de AWS podrían provocar alguna pérdida de datos durante la grabación de la transmisión. En estos casos, Amazon IVS prioriza la transmisión en directo sobre la grabación. Para obtener redundancia, grabe localmente a través de su herramienta de streaming.

Para obtener más información (incluido cómo configurar el procesamiento posterior o la reproducción de VOD en los archivos grabados), consulte [Grabación de participantes individuales](rt-individual-participant-recording.md).

### Cómo deshabilitar la grabación
<a name="getting-started-disable-recording"></a>

Para deshabilitar la grabación en Amazon S3 en una fase existente:
+ Consola: en la página de detalles de la fase correspondiente, en la sección de las transmisiones **Record individual participants** (Grabación de participantes individuales), desactive la opción **Habilitar la grabación automática** en la sección **Auto-record to S3** (Grabar automáticamente en S3) y, a continuación, seleccione **Guardar cambios**. Esto elimina la asociación de la configuración de almacenamiento con la fase; las transmisiones en esa fase ya no se grabarán.
+ CLI: ejecute el comando `update-stage` y pase el ARN de la configuración de grabación como una cadena vacía:

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

  Esto devuelve un objeto de fase con una cadena vacía para `storageConfigurationArn`, lo que indica que la grabación está deshabilitada.

## Instrucciones de la consola para crear una fase de IVS
<a name="getting-started-create-stage-console"></a>

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

   (También puede acceder a la consola de Amazon IVS a través de la [consola de administración de AWS](https://console.aws.amazon.com/)).

1. En el panel de navegación de la izquierda, selecciona **Fases** y, a continuación, seleccione **Crear fase**. Aparece la ventana **Crear fase**.  
![\[Utilice la ventana Crear fase para crear una nueva fase y un token de participante para ella.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Create_Stage_Console_IPR.png)

1. Si lo desea, ingrese un **nombre para la fase**.

1. Si desea habilitar la grabación de participantes individuales, complete los pasos que se indican a continuación en [Configuración de la grabación automática de participantes individuales en Amazon S3 (opcional)](#getting-started-create-stage-ipr).

1. Seleccione **Crear fase** para crear la fase. Aparece la página de detalles de la nueva fase.

### Configuración de la grabación automática de participantes individuales en Amazon S3 (opcional)
<a name="getting-started-create-stage-ipr"></a>

Siga estos pasos para habilitar la grabación de participantes individuales al crear una fase:

1. En la página **Crear fase**, en **Record individual participants** (Grabación de participantes individuales), active la opción **Habilitar la grabación automática**. Aparecen campos adicionales para elegir los **tipos de medios grabado**, elegir una **configuración de almacenamiento** existente o crear una nueva y elegir si se van a grabar las miniaturas a intervalos.  
![\[Utilice el cuadro de diálogo “Record individual participants” (Grabación de participantes individuales) para configurar la grabación de participantes individuales para una fase.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Create_Stage_Console_enable_IPR.png)

1. Elija los tipos de medios que desee grabar.

1. Seleccione **Create storage configuration** (Crear configuración de almacenamiento). Se abrirá una nueva ventana con opciones de almacenamiento para crear un bucket de Amazon S3 y asociarlo a la nueva configuración de grabación.  
![\[Utilice la ventana “Create storage configuration” (Crear configuración de almacenamiento) para crear una nueva configuración de almacenamiento para una fase.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Create_Storage_Configuration_IPR.png)

1. Rellene los campos:

   1. Si lo desea, introduzca un **nombre de configuración de almacenamiento**.

   1. Introduzca un **Nombre de bucket**.

1. Seleccione **Create storage configuration** (Crear configuración de almacenamiento) para crear un nuevo recurso de configuración de almacenamiento con un ARN único. Normalmente, la creación de la configuración de grabación tarda unos segundos, pero puede tomar hasta 20 segundos. Cuando se cree la configuración de almacenamiento, vuelve a aparecer la ventana **Crear fase**. Allí, en el área **Record individual participants** (Grabació de participantes individuales), se muestra la nueva **configuración de almacenamiento** y el bucket de S3 (**Almacenamiento**) que creó.  
![\[Creación de una fase con la consola de IVS: se creó una nueva configuración de almacenamiento.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Create_Stage_Console_Storage_Configuration.png)

1. Si lo desea, puede activar otras opciones no predeterminados, como la grabación de réplicas de participantes, la combinación de las grabaciones de los participantes individuales y la grabación en miniatura.  
![\[Cree una fase con la consola de IVS: active opciones avanzadas como la grabación de miniaturas y la unión de IPR.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Create_Stage_Console_IPR_Stitching.png)

## Instrucciones de la CLI para crear una fase de IVS
<a name="getting-started-create-stage-cli"></a>

Para instalar la AWS CLI, consulte [Install or update to the latest version of the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).

Ahora puede usar la CLI para crear y administrar recursos siguiendo uno de los dos procedimientos siguientes en función de si desea crear una fase con la grabación de participantes individuales habilitada o deshabilitada.

### Creación de una fase sin la grabación de participantes individuales
<a name="getting-started-create-stage-cli-without-ipr"></a>

La API de escenarios se encuentra en el espacio de nombres ivs-realtime. Por ejemplo, para crear un escenario:

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

La respuesta es:

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

### Creación de una fase con la grabación de participantes individuales
<a name="getting-started-create-stage-cli-with-ipr"></a>

Para crear una fase con la grabación de participantes individuales habilitada:

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

De manera opcional, transfiera el parámetro `thumbnailConfiguration` para configurar manualmente el modo de grabación y almacenamiento de miniaturas y el intervalo de miniaturas en segundos:

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

Opcionalmente, pase el parámetro `recordingReconnectWindowSeconds` para habilitar la combinación de grabaciones fragmentadas de participantes individuales:

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

La respuesta es:

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

# Paso 3: distribuir los tokens de participantes
<a name="getting-started-distribute-tokens"></a>

Ahora que tiene una fase, debe crear y distribuir tokens a los participantes para que puedan unirse a la fase y empezar a enviar y recibir videos. Existen dos enfoques para generar tokens:
+ [Creación](#getting-started-distribute-tokens-self-signed) de tokens con un par de claves.
+ [Creación de tokens con la API de transmisión en tiempo real de IVS](#getting-started-distribute-tokens-api).

A continuación se describen estos dos enfoques.

## Creación de tokens con un par de claves
<a name="getting-started-distribute-tokens-self-signed"></a>

Puede crear tokens en la aplicación de servidor y distribuirlos entre los participantes para que se unan a una fase. Tiene que generar un par de claves públicas o privadas ECDSA para firmar los JWT e importar la clave pública en IVS. Luego, IVS puede verificar los tokens al unirse a la fase. 

IVS no ofrece caducidad de claves. Si la clave privada está comprometida, debe eliminar la clave pública anterior.

### Creación de un nuevo par de claves
<a name="getting-started-distribute-tokens-self-signed-create-key-pair"></a>

Existen varias formas de crear un par de claves. A continuación, damos dos ejemplos.

Para crear un par de claves nuevo en la consola, siga estos pasos:

1. [Abra la consola de Amazon IVS](https://console.aws.amazon.com/ivs). Elija la región de su fase si aún no está en ella.

1. En el menú de navegación izquierdo, elija **Transmisión en tiempo real > Claves públicas**.

1. Elija **Crear clave pública**. Aparece el cuadro de diálogo **Crear clave pública**.

1. Siga las indicaciones y elija **Create (Crear)**.

1. Amazon IVS genera un nuevo par de claves. La clave pública se importa como un recurso de clave pública y la clave privada se pone inmediatamente a disposición para su descarga. La clave pública también se puede descargar más adelante si es necesario.

   Amazon IVS genera la clave en el lado del cliente y no almacena la clave privada. ***Asegúrese de guardar la clave; no podrá recuperarla más tarde.***

Para crear un par de claves nuevo EC P384 con OpenSSL (es posible que tenga que instalar [OpenSSL](https://www.openssl.org/source/) primero), siga los pasos a continuación. Este proceso le permite acceder tanto a la clave privada como a la pública. Solo necesita la clave pública si quiere probar la verificación de los tokens.

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

Ahora, importe la clave pública nueva con las instrucciones que se incluyen a continuación.

### Importación de la clave pública
<a name="getting-started-distribute-tokens-import-public-key"></a>

Una vez que tenga un par de claves, puede importar la clave pública a IVS. Nuestro sistema no necesita la clave privada, pero usted la utiliza para firmar los tokens.

Para importar una clave pública actual con la consola:

1. [Abra la consola de Amazon IVS](https://console.aws.amazon.com/ivs). Elija la región de su fase si aún no está en ella.

1. En el menú de navegación izquierdo, elija **Transmisión en tiempo real > Claves públicas**.

1. Seleccione **Importar**. Aparece el cuadro de diálogo **Importar clave pública**.

1. Siga las indicaciones y elija **Import (Importar)**.

1. Amazon IVS importa la clave pública y genera un recurso de clave de pública.

Para importar una clave pública actual con la CLI:

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

Puede omitir `--region <aws-region>` si la región se encuentra en su archivo de configuración local de AWS.

Aquí tiene un ejemplo de respuesta:

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

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

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

### Generación y firma del token
<a name="getting-started-distribute-tokens-self-signed-generate-sign"></a>

Para obtener detalles sobre cómo trabajar con JWT y las bibliotecas compatibles para firmar tokens, visite [jwt.io](https://jwt.io/). En la interfaz jwt.io, debes introducir la clave privada para firmar los tokens. La clave pública solo es necesaria si desea verificar los tokens.

Todos los JWT tienen tres campos: encabezado, carga y firma.

Los esquemas JSON para el encabezado y la carga útil del JWT se describen a continuación. Como alternativa, puede copiar un JSON de muestra de la consola de IVS. Para obtener el JSON de encabezado y carga útil desde la consola de IVS:

1. [Abra la consola de Amazon IVS](https://console.aws.amazon.com/ivs). Elija la región de su fase si aún no está en ella.

1. En el menú de navegación izquierdo, elija **Transmisión en tiempo real > Fases**.

1. Seleccione la fase que desea usar. Seleccione **Ver detalles**.

1. En la sección **Tokens de participantes**, seleccione el menú desplegable situado junto a **Crear token**.

1. Seleccione **Cree el encabezado y la carga útil del token**.

1. Rellene el formulario y copie el encabezado y la carga útil de JWT que se muestran en la parte inferior de la ventana emergente.

#### Esquema del token: encabezado
<a name="getting-started-distribute-tokens-self-signed-generate-sign-header"></a>

El encabezado especifica lo siguiente:
+ `alg` es el algoritmo de firma. Este es ES384, un algoritmo de firma ECDSA que utiliza el algoritmo hash SHA-384.
+ `typ` es el tipo de token, JWT.
+ `kid` es el ARN de la clave pública utilizada para firmar el token. Debe ser el mismo ARN devuelto por la solicitud a la API [GetPublicKey](https://docs.aws.amazon.com//ivs/latest/RealTimeAPIReference/API_GetPublicKey.html).

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

#### Esquema del token: carga útil
<a name="getting-started-distribute-tokens-self-signed-generate-sign-payload"></a>

La carga útil contiene datos específicos de IVS. Todos los campos excepto `user_id` son obligatorios.
+ `RegisteredClaims` en la especificación de JWT son notificaciones reservadas que deben proporcionarse para que el token de la fase sea válido: 
  + `exp` (fecha de vencimiento) es una marca de tiempo UTC Unix del momento en que caduca el token. (Una marca de tiempo Unix es un valor numérico que representa los segundos desde 1970-01-01T00:00:00Z UTC hasta la fecha y hora UTC especificadas, sin tener en cuenta los segundos intercalares). El token se valida cuando el participante se une a una fase. IVS proporciona los tokens con un TTL predeterminado de 12 horas, que es el valor que recomendamos; se puede ampliar hasta un máximo de 14 días a partir del momento de su emisión (iat). Debe ser un valor de tipo entero.
  + `iat` (momento de la emisión) es una marca de tiempo UTC Unix del momento en que se emitió el JWT. (Consulte la nota para `exp` sobre las marcas de tiempo Unix). Debe ser un valor de tipo entero.
  + `jti` (ID de JWT) es el ID del participante que se utiliza para rastrear el participante al que se concede el token y para hacer referencia a él. Cada token debe tener un ID de participante único. Debe ser una cadena que distinga mayúsculas de minúsculas, con una longitud máxima de 64 caracteres, que contenga solo caracteres alfanuméricos, guiones (-) y guiones bajos (\$1). No se permite utilizar ningún otro carácter especial. 
+ `user_id` es un nombre opcional asignado por el cliente para ayudar a identificar el token; se puede usar para vincular a un participante con un usuario en los propios sistemas del cliente. Debe coincidir con el campo `userId` de la solicitud a la API [CreateParticipantToken](https://docs.aws.amazon.com/ivs/latest/RealTimeAPIReference/API_CreateParticipantToken.html). Puede ser cualquier texto codificado en UTF-8 y es una cadena de hasta 128 caracteres. *Este campo se expone a todos los participantes de la fase y no se debe utilizar en la información de identificación personal ni confidencial.*
+ `resource` es el ARN de la fase; p. ej., `arn:aws:ivs:us-east-1:123456789012:stage/oRmLNwuCeMlQ`.
+ `topic` es el ID de la fase, que se puede extraer del ARN de la fase. Por ejemplo, si el ARN de la fase es `arn:aws:ivs:us-east-1:123456789012:stage/oRmLNwuCeMlQ`, el ID de la fase es `oRmLNwuCeMlQ`.
+ `events_url` debe ser el punto de conexión del evento devuelto por la operación CreateStage o GetStage. Se recomienda almacenar en caché este valor en el momento de la creación de la fase; el valor se puede almacenar en caché durante un máximo de 14 días. Un valor de ejemplo es `wss://global.events.live-video.net`.
+ `whip_url` debe ser el punto de conexión WHIP devuelto por la operación CreateStage o GetStage. Se recomienda almacenar en caché este valor en el momento de la creación de la fase; el valor se puede almacenar en caché durante un máximo de 14 días. Un valor de ejemplo es `https://453fdfd2ad24df.global-bm.whip.live-video.net`.
+ `capabilities` especifica las capacidades del token; los valores válidos son `allow_publish` y `allow_subscribe`. Para los tokens de solo suscripción, establezca únicamente `allow_subscribe` en `true`.
+ `attributes` es un campo opcional en el que puede especificar los atributos proporcionados por la aplicación para codificarlos en el token y adjuntarlos a una fase. Las claves y valores de asignación pueden contener texto codificado en UTF-8. La longitud máxima de este campo es de 1 KB en total. *Este campo se expone a todos los participantes de la fase y no se debe utilizar en la información de identificación personal ni confidencial.*
+ `version` debe ser `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"
  }
  ```

#### Esquema del token: firma
<a name="getting-started-distribute-tokens-self-signed-generate-sign-signature"></a>

Para crear la firma, utilice la clave privada con el algoritmo especificado en el encabezado (ES384) para firmar el encabezado codificado y la carga codificada.

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

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

1. Para generar la firma del token con un algoritmo de firma ES384, firme el algoritmo y una clave privada asociada a la clave pública proporcionada a IVS.

1. Crear el token.

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

## Creación de tokens con la API de transmisión en tiempo real de IVS
<a name="getting-started-distribute-tokens-api"></a>

![\[Distribución de los tokens de participante: flujo de trabajo con tokens de escenario\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Distribute_Participant_Tokens.png)


Como se muestra anteriormente, una aplicación cliente solicita un token a su aplicación del lado del servidor y esta llama a CreateParticipantToken mediante un SDK de AWS o una solicitud SigV4 firmada. Como las credenciales de AWS se utilizan para llamar a la API, el token debe generarse en una aplicación segura del lado del servidor, no en la aplicación del lado del cliente.

Al crear un token de participante, puede especificar opcionalmente atributos o capacidades:
+ Puede especificar los atributos proporcionados por la aplicación para codificarlos en el token y adjuntarlos a una fase. Las claves y valores de asignación pueden contener texto codificado en UTF-8. La longitud máxima de este campo es de 1 KB en total. *Este campo se expone a todos los participantes de la fase y no se debe utilizar en la información de identificación personal ni confidencial.*
+ Puede especificar las capacidades que habilita el token. El valor predeterminado es `PUBLISH` y `SUBSCRIBE`, que permite al participante enviar y recibir audio y video, pero puede emitir tokens con un subconjunto de capacidades. Por ejemplo, puede emitir un token que tenga únicamente la capacidad `SUBSCRIBE` para los moderadores. En ese caso, los moderadores podrían ver a los participantes que envían videos, pero no enviar los suyos propios.

Para obtener más información, consulte [CreateParticipantToken](https://docs.aws.amazon.com//ivs/latest/RealTimeAPIReference/API_CreateParticipantToken.html).

Puede crear tokens de participantes mediante la consola o la CLI para llevar a cabo pruebas y desarrollar, pero lo más probable es que le convenga más crearlos con el SDK de AWS en su entorno de producción.

Tendrá que distribuir los tokens de su servidor a cada cliente (por ejemplo, mediante una solicitud a la API). No ofrecemos esta funcionalidad. Para esta guía, basta con seguir estos pasos para copiar y pegar los tokens en el código del cliente:

**Importante**: trate los tokens como elementos opacos; es decir, no cree funciones en función del contenido del token. El formato de los tokens podría cambiar en el futuro.

### Instrucciones de la consola
<a name="getting-started-distribute-tokens-console"></a>

1. Vaya al escenario que creó en el paso anterior.

1. Seleccione **Crear token**. Aparece la ventana **Crear token**.

1. Ingrese un ID de usuario para asociarlo al token. Puede ser cualquier texto codificado en UTF-8. 

1. Seleccione **Crear**.

1. Copie el token. *Importante: Asegúrese de guardar el token; IVS no lo almacena y no podrá recuperarlo más adelante*.

### Instrucciones de la CLI
<a name="getting-started-distribute-tokens-cli"></a>

Crear un token con la AWS CLI requiere que antes descargue y configure la CLI en su equipo. Para obtener más información, consulte la [Guía del usuario de la interfaz de línea de comandos de AWS](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html). Nota: La generación de tokens con la AWS CLI es una buena opción para hacer pruebas, pero, para el uso en producción, le recomendamos que genere tokens en el lado del servidor con el SDK de AWS (consulte las instrucciones a continuación).

1. Ejecute el comando `create-participant-token` con el ARN del escenario. Incluya cualquiera de las siguientes capacidades: `"PUBLISH"`, `"SUBSCRIBE"`.

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

1. Esto devuelve un token de participante:

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

1. Guarde este token. Lo necesitará para unirse al escenario y enviar y recibir videos.

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

Puede utilizar el SDK de AWS para crear tokens. A continuación, se muestran las instrucciones para el SDK de AWS mediante JavaScript. 

**Importante:** Este código debe ejecutarse en el lado del servidor y su salida se debe pasar al cliente.

**Requisito previo:** para usar el ejemplo de código que aparece a continuación, debe instalar el paquete aws-sdk/client-ivs-realtime. Para obtener más información, consulte [Introducción a SDK de AWS para 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);
```

# Paso 4: integrar el SDK de transmisión de IVS
<a name="getting-started-broadcast-sdk"></a>

IVS proporciona un SDK de transmisión para Android, iOS y web que puede integrar en su aplicación. El SDK de transmisión se utiliza tanto para enviar como para recibir videos. Si ha [configurado la ingesta RTMP para su fase](https://docs.aws.amazon.com/ivs/latest/RealTimeUserGuide/rt-stream-ingest.html), puede utilizar cualquier codificador que pueda transmitir a un punto de conexión de RTMP (por ejemplo, OBS o ffmpeg).

En esta sección, escribimos una aplicación sencilla que permite que dos o más participantes interactúen en tiempo real. Siga los pasos que se indican a continuación para crear una aplicación llamada BasicRealTime. El código completo de la aplicación está en CodePen y GitHub:
+  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-transmisión-android-samples](https://github.com/aws-samples/amazon-ivs-real-time-streaming-android-samples) 
+  iOS: [https://github.com/aws-samples/amazon-ivs-real-time-transmisión-ios-samples](https://github.com/aws-samples/amazon-ivs-real-time-streaming-ios-samples) 

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

### Configuración de archivos
<a name="getting-started-broadcast-sdk-web-setup"></a>

Para empezar, cree una carpeta y un archivo HTML y JS inicial para configurar los archivos:

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

Puede instalar el SDK de transmisión mediante una etiqueta script o npm. Nuestro ejemplo usa la etiqueta script por motivos de simplicidad, pero es fácil de modificar si opta por usar npm más adelante.

### Uso de una etiqueta de script
<a name="getting-started-broadcast-sdk-web-script"></a>

El SDK de transmisión web se distribuye como biblioteca de JavaScript y se puede obtener en [https://web-broadcast.live-video.net/1.33.0/amazon-ivs-web-broadcast.js](https://web-broadcast.live-video.net/1.33.0/amazon-ivs-web-broadcast.js).

Cuando se carga mediante una etiqueta `<script>`, la biblioteca muestra una variable global en el ámbito del intervalo denominado `IVSBroadcastClient`.

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

Para instalar el paquete de npm:

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

Ahora puede acceder al objeto IVSBroadcastClient:

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

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

### Creación del proyecto para Android
<a name="getting-started-broadcast-sdk-android-project"></a>

1. Cree un **nuevo proyecto** en Android Studio.

1. Elija **Actividad de vistas vacías**.

   Nota: En algunas versiones anteriores de Android Studio, la actividad basada en vistas se denomina **Actividad vacía**. Si aparece la ventana de Android Studio **Actividad vacía** y *no* **Actividad de vistas vacías**, seleccione **Actividad vacía**. De lo contrario, no seleccione **Actividad vacía**, ya que utilizaremos las API de vistas (no Jetpack Compose).

1. Asigne un **nombre** a su proyecto y, a continuación, seleccione **Finalizar**.

### Instalación del SDK de transmisión
<a name="getting-started-broadcast-sdk-android-install"></a>

A fin de agregar la biblioteca de transmisión de Android de Amazon IVS a su entorno de desarrollo de Android, agregue la biblioteca al archivo `build.gradle` del módulo como se muestra a continuación (para la versión más reciente del SDK de transmisión de Amazon IVS). En los proyectos más recientes, es posible que el repositorio `mavenCentral` ya esté incluido en su archivo `settings.gradle`, si ese es el caso, puede omitir el bloqueo de `repositories`. Para nuestro ejemplo, también necesitaremos habilitar el enlace de datos en el bloque `android`.

```
android {
    dataBinding.enabled true
}

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

Alternativamente, para instalar el SDK de forma manual, descargue la última versión desde esta ubicación:

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

### Creación del proyecto para iOS
<a name="getting-started-broadcast-sdk-ios-project"></a>

1. Cree un nuevo proyecto de Xcode.

1. En **Plataforma**, seleccione **iOS**.

1. En **Aplicación**, seleccione **Aplicación**.

1. Ingrese el **nombre del producto** de su aplicación y, a continuación, seleccione **Siguiente**.

1. Para elegir un directorio en el que guardar el proyecto, vaya a este y, a continuación, seleccione **Crear**.

A continuación, tiene que incorporar el SDK. Para obtener instrucciones, consulte [Instalación de la biblioteca](broadcast-ios-getting-started.md#broadcast-ios-install) en la *Guía del SDK de transmisión para iOS*.

### Configuración de permisos
<a name="getting-started-broadcast-sdk-ios-config"></a>

Tiene que actualizar el `Info.plist` de su proyecto para agregar dos entradas nuevas para `NSCameraUsageDescription` y `NSMicrophoneUsageDescription`. En los valores, proporcione explicaciones para el usuario sobre por qué la aplicación solicita acceso a la cámara y al micrófono.

![\[Configure los permisos de iOS.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/iOS_Configure.png)


# Paso 5: publicar y suscribirse a videos
<a name="getting-started-pub-sub"></a>

Puede publicar o suscribirse (en tiempo real) en IVS con:
+ Los [SDK de transmisión nativos de IVS](https://docs.aws.amazon.com//ivs/latest/LowLatencyUserGuide/getting-started-set-up-streaming.html#broadcast-sdk), que admiten WebRTC y RTMPS. Lo recomendamos, especialmente para escenarios de producción. Consulte los detalles que aparecen a continuación para la [web](getting-started-pub-sub-web.md), [Android](getting-started-pub-sub-android.md) y [iOS](getting-started-pub-sub-ios.md).
+ La consola de Amazon IVS: es adecuada para probar transmisiones. Consulte a continuación.
+ Otros codificadores de software y hardware de transmisión: puede usar cualquier codificador de transmisión que sea compatible con los protocolos RTMP, RTMPS o WHIP. Para obtener más información, consulte [Ingesta de transmisiones](rt-stream-ingest.md).

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

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

   (También puede obtener acceso a la consola de Amazon IVS mediante la [consola de administración de AWS](https://console.aws.amazon.com/)).

1. En el panel de navegación, seleccione **Fases**. (Si el panel de navegación está contraído, amplíelo seleccionando el icono de la hamburguesa).

1. Seleccione la fase a la que desee suscribirse o en la que quiera publicar para ir a su página de detalles.

1. Suscripción: si la fase tiene uno o más publicadores, puede pulsar el botón **Suscribirse**, en la pestaña **Suscribirse**, para suscribirse a ella. (Las pestañas están debajo de la sección **Configuración general**).

1. Publicación:

   1. Seleccione la pestaña **Publicar**.

   1. Se le pedirá que conceda a la consola de IVS acceso a su cámara y micrófono; **permita** esos permisos.

   1. En la parte inferior de la pestaña **Publicar**, utilice los cuadros desplegables para seleccionar los dispositivos de entrada para el micrófono y la cámara.

   1. Para empezar a publicar, seleccione **Empezar a publicar**.

   1. Para ver el contenido publicado, vuelva a la pestaña **Suscribirse**.

   1. Para dejar de publicar, vaya a la pestaña **Publicar** y pulse el botón **Dejar de publicar** situado hacia la parte inferior.

**Nota**: La suscripción y la publicación consumen recursos y se le cobrará una tarifa por hora durante el tiempo que permanezca conectado a la fase. Para obtener más información, consulte [Streaming en tiempo real](https://aws.amazon.com/ivs/pricing/#Real-Time_Streaming) en la página de precios de IVS.

# Publicación y suscripción con el SDK de transmisión para web de IVS
<a name="getting-started-pub-sub-web"></a>

En esta sección se explican los pasos necesarios para publicar y suscribirse a una fase mediante una aplicación web.

## Creación de una plantilla HTML reutilizable
<a name="getting-started-pub-sub-web-html"></a>

Primero, creemos la plantilla HTML reutilizable e importemos la biblioteca como una etiqueta script:

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

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

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

<body>

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

</body>
</html>
```

## Configuración de la entrada de tokens y adición de botones de unión y salida
<a name="getting-started-pub-sub-web-join"></a>

Aquí completamos el cuerpo con nuestros controles de entrada. Estos toman como entrada el token y configuran botones de **unión** y **salida**. Por lo general, las aplicaciones solicitarán el token a la API de la aplicación, pero, en este ejemplo, tiene que copiar y pegar el token en la entrada del 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 />
```

## Adición de elementos de contenedor multimedia
<a name="getting-started-pub-sub-web-media"></a>

Estos elementos albergarán el contenido multimedia para nuestros participantes locales y remotos. Agregamos una etiqueta script para cargar la lógica de nuestra aplicación definida en `app.js`.

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

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

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

Esto completa la página HTML y debería ver esto al cargar `index.html` en un navegador:

![\[Vea transmisión en tiempo real en un navegador: configuración del HTML completada.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/RT_Browser_View.png)


## Creación de app.js
<a name="getting-started-pub-sub-web-appjs"></a>

Pasemos a definir el contenido de nuestro archivo `app.js`. Comience por importar todas las propiedades necesarias de la versión global del SDK:

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

## Creación de las variables de la aplicación
<a name="getting-started-pub-sub-web-vars"></a>

Configure variables que contengan referencias a nuestros botones HTML de **unión** y **salida** y al estado de la aplicación:

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

## Creación de joinStage 1: definición de la función y validación de la entrada
<a name="getting-started-pub-sub-web-joinstage1"></a>

La función `joinStage` toma el token de entrada, crea una conexión con el escenario y comienza a publicar el video y el audio recuperados de `getUserMedia`.

Para empezar, definimos la función y validamos el estado y la entrada del token. Vamos a desarrollar esta función en las próximas secciones.

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

## Creación de joinStage 2: obtención de contenido multimedia para su publicación
<a name="getting-started-pub-sub-web-joinstage2"></a>

Este es el contenido multimedia que se publicará en el escenario:

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

## Creación de joinStage 3: definición de la estrategia escénica y creación del escenario
<a name="getting-started-pub-sub-web-joinstage3"></a>

Esta estrategia de escenarios es la base de la lógica de decisión que utiliza el SDK para decidir qué publicar y a qué participantes suscribirse. Para obtener más información sobre el propósito de la función, consulte [Estrategia](web-publish-subscribe.md#web-publish-subscribe-concepts-strategy).

Esta estrategia es sencilla. Cuando se una al escenario, publique las transmisiones que acabamos de recuperar y suscríbase al contenido de audio y video de todos los participantes remotos:

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

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

## Creación de joinStage 4: gestión de los eventos del escenario y renderización del contenido multimedia
<a name="getting-started-pub-sub-web-joinstage4"></a>

Los escenarios emiten muchos eventos. Tendremos que escuchar los eventos `STAGE_PARTICIPANT_STREAMS_ADDED` y `STAGE_PARTICIPANT_LEFT` para renderizar y eliminar contenido multimedia hacia y desde la página. Puede encontrar un conjunto más exhaustivo de eventos en [Eventos](web-publish-subscribe.md#web-publish-subscribe-concepts-events).

Tenga en cuenta que creamos cuatro funciones auxiliares para poder administrar los elementos del DOM necesarios: `setupParticipant`, `teardownParticipant`, `createVideoEl` y `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;
}
```

## Creación de joinStage 5: unión al escenario
<a name="getting-started-pub-sub-web-joinstage5"></a>

Vamos a unirnos por fin al escenario para completar la función `joinStage`.

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

## Creación de leaveStage
<a name="getting-started-pub-sub-web-leavestage"></a>

Defina la función `leaveStage` que invocará el botón de salida.

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

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

## Inicialización de los controladores de eventos de entrada
<a name="getting-started-pub-sub-web-handlers"></a>

Agregaremos una última función al archivo `app.js`. Esta función se invoca inmediatamente cuando se carga la página y establece controladores de eventos para unirse al escenario y salir de este.

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

## Ejecute la aplicación y proporcione un token
<a name="getting-started-pub-sub-run-app"></a>

En este punto, puede compartir la página web de forma local o con otras personas, [abrir la página](#getting-started-pub-sub-web-media), introducir un token de participante y unirte al escenario.

## Pasos siguientes
<a name="getting-started-pub-sub-next"></a>

Para ver ejemplos más detallados sobre npm, React y otros, consulta la [Guía web del SDK de transmisión de IVS (Guía de transmisión en tiempo real)](broadcast-web.md).

# Publicación y suscripción con el SDK de transmisión para Android de IVS
<a name="getting-started-pub-sub-android"></a>

En esta sección se explican los pasos necesarios para publicar y suscribirse a una fase mediante una aplicación Android.

## Creación de vistas
<a name="getting-started-pub-sub-android-views"></a>

Empezamos por crear un diseño simple para nuestra aplicación con el archivo `activity_main.xml` creado automáticamente. El diseño contiene `EditText` para agregar un token, un `Button` de unión, `TextView` para mostrar el estado del escenario y `CheckBox` para cambiar la publicación.

![\[Configure el diseño de publicación de su aplicación de Android.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_Android_1.png)


Este es el XML que hay detrás de la vista:

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

Aquí hacemos referencia a los ID de un par de cadenas, así que es el momento de crear todo el archivo `strings.xml`:

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

Vamos a vincular esas vistas del XML a nuestro `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)
}
```

Ahora crearemos una vista de elementos para `RecyclerView`. Para ello, haga clic con el botón derecho en el directorio `res/layout` y seleccione **Nuevo > Archivo de recursos de diseño**. Asigne a este archivo el nombre `item_stage_participant.xml`.

![\[Cree una vista de elementos para su aplicación de Android RecyclerView.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_Android_2.png)


El diseño de este elemento es sencillo: contiene una vista para renderizar la transmisión de video de un participante y una lista de etiquetas para mostrar información sobre el participante:

![\[Cree una vista de elementos para su aplicación de Android RecyclerView: etiquetas.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_Android_3.png)


Este es el XML:

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

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

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

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

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

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

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

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

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

    </LinearLayout>

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

Este archivo XML infla una clase que aún no hemos creado, `ParticipantItem`. Como el XML incluye el espacio de nombres completo, asegúrese de actualizar este archivo XML en su espacio de nombres. Vamos a crear esta clase y configurar las vistas, aunque lo vamos a dejar en blanco por ahora.

Cree una nueva clase de Kotlin, `ParticipantItem`:

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

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

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

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

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

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

Para utilizar la cámara y el micrófono, debe solicitar permisos al usuario. Para ello, seguimos un flujo de permisos estándar:

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

## Estado de la aplicación
<a name="getting-started-pub-sub-android-app-state"></a>

Nuestra aplicación hace un seguimiento de los participantes en el entorno local en un archivo `MainViewModel.kt`. El estado se comunicará de nuevo a `MainActivity` mediante [StateFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/) de Kotlin.

Cree una nueva clase de Kotlin (`MainViewModel`):

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

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

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

}
```

En `MainActivity.kt`, administramos el modelo de la vista:

```
import androidx.activity.viewModels

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

Para usar `AndroidViewModel` y las extensiones `ViewModel` de Kotlin, tendrá que agregar lo siguiente al archivo `build.gradle` del módulo:

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

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

Crearemos una subclase `RecyclerView.Adapter` sencilla para hacer un seguimiento de nuestros participantes y actualizar `RecyclerView` en los eventos del escenario. Pero antes, necesitamos una clase que represente a un participante. Cree una nueva clase de Kotlin (`StageParticipant`):

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

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

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

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

Usaremos esta clase en la clase `ParticipantAdapter` que crearemos a continuación. Empezamos por definir la clase y crear una variable para hacer un seguimiento de los participantes:

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

También tenemos que definir `RecyclerView.ViewHolder` antes de implementar el resto de las anulaciones:

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

Con esto, podemos implementar las anulaciones de `RecyclerView.Adapter` estándar:

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

Por último, agregamos nuevos métodos a los que llamaremos desde `MainViewModel` cuando se hagan cambios en los participantes. Estos métodos son operaciones CRUD estándar en el adaptador.

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

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

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

En `MainViewModel`, tenemos que crear y mantener una referencia a este adaptador:

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

## Estado de la etapa
<a name="getting-started-pub-sub-android-views-stage-state"></a>

También tenemos que hacer un seguimiento del estado de algunos escenarios en `MainViewModel`. Definamos esas propiedades ahora:

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

Para ver su propia vista previa antes de unirse a un escenario, creamos inmediatamente un participante local:

```
init {
    deviceDiscovery = DeviceDiscovery(application)

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

Queremos asegurarnos de borrar estos recursos cuando se elimine `ViewModel`. Anulamos `onCleared()` directamente para así no olvidarnos de eliminar estos recursos.

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

A continuación, completamos la propiedad `streams` local nada más recibir los permisos e implementamos el método `permissionsGranted` al que llamamos anteriormente:

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

## Implementación del SDK de escenarios
<a name="getting-started-pub-sub-android-stage-sdk"></a>

Los siguientes tres [conceptos](android-publish-subscribe.md#android-publish-subscribe-concepts) básicos subyacen a la funcionalidad de transmisión en tiempo real: escenario, estrategia y renderizador. El objetivo del diseño es minimizar la cantidad de lógica necesaria por parte del cliente para crear un producto que funcione.

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

La implementación de `Stage.Strategy` es sencilla:

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

En resumen, la publicación depende del estado de la variable `publishEnabled` interna y, si publicamos, publicaremos las transmisiones que recopilamos anteriormente. Por último, en este ejemplo, siempre nos suscribimos a otros participantes y recibimos tanto su contenido de audio como de video.

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

La implementación de `StageRenderer` también es bastante simple, aunque, dada la cantidad de funciones, contiene bastante más código. El enfoque general de este renderizador es actualizar `ParticipantAdapter` cuando el SDK nos notifique un cambio en un participante. Hay ciertos casos en los que tratamos a los participantes locales de forma diferente, porque hemos decidido administrarlos por nuestra cuenta para que puedan ver la vista previa de la cámara antes de unirse.

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

## Implementación de LayoutManager de RecyclerView personalizado
<a name="getting-started-pub-sub-android-layout"></a>

Establecer diferentes números de participantes puede resultar complejo. Lo ideal es que ocupen todo el marco de la vista principal, pero gestionar la configuración de cada participante de forma independiente no es la mejor forma de lograrlo. Para facilitarlo, veremos cómo implementar `RecyclerView.LayoutManager`.

Cree otra clase, `StageLayoutManager`, que debería ampliar `GridLayoutManager`. Esta clase está pensada para calcular cómo se mostrará cada participante en función del número de participantes en un diseño de filas o columnas basado en flujos. Cada fila tiene la misma altura que las demás, pero las columnas pueden tener diferentes anchuras por fila. Consulte el comentario del código que aparece sobre la variable `layouts` para ver cómo personalizar este comportamiento.

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

En `MainActivity.kt`, tenemos que configurar el adaptador y el administrador de diseño de `RecyclerView`:

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

## Enlace de acciones de la interfaz de usuario
<a name="getting-started-pub-sub-android-actions"></a>

Ya casi está, solo tenemos que enlazar algunas acciones de la interfaz de usuario.

Primero, tenemos que hacer que `MainActivity` supervise los cambios en `StateFlow` desde `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)
        }
    }
}
```

A continuación, agregamos los oyentes a nuestro botón de unión y a la casilla de publicación:

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

Ambos elementos llaman a funciones en `MainViewModel`, lo cual implementaremos ahora:

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

## Renderización de los participantes
<a name="getting-started-pub-sub-android-participants"></a>

Por último, tenemos que renderizar los datos que recibimos del SDK en el elemento de participante que creamos anteriormente. Ya hemos completado la lógica de `RecyclerView`, por lo que solo tenemos que implementar la API `bind` en `ParticipantItem`.

Empezamos por agregar la función vacía y, luego, la analizaremos paso a paso:

```
fun bind(participant: StageParticipant) {

}
```

Primero, analizaremos el estado sencillo, el ID del participante, el estado de la publicación y el estado de la suscripción. Para hacerlo, tan solo actualizamos `TextViews` directamente:

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

A continuación, actualizaremos los estados silenciados de audio y video. Para obtener el estado silenciado, tenemos que encontrar el `ImageDevice` y el `AudioDevice` de la matriz de transmisiones. Para optimizar el rendimiento, recordamos los últimos ID de los dispositivos conectados.

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

Por último, queremos renderizar una vista previa de `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
```

Y mostramos las estadísticas de audio de `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
```

# Publicación y suscripción con el SDK de transmisión para iOS de IVS
<a name="getting-started-pub-sub-ios"></a>

En esta sección se explican los pasos necesarios para publicar y suscribirse a una fase mediante una aplicación iOS.

## Creación de vistas
<a name="getting-started-pub-sub-ios-views"></a>

Lo primero es usar el archivo `ViewController.swift` creado automáticamente para importar `AmazonIVSBroadcast` y, luego, agregar algunas `@IBOutlets` que vincular:

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

Ahora creamos esas vistas y las vinculamos en `Main.storyboard`. Esta es la estructura de vistas que utilizaremos:

![\[Use Main.storyboard para crear una vista de iOS.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_iOS_1.png)


Para configurar AutoLayout, tenemos que personalizar tres vistas. La primera vista es **Vista de colección de participantes** (una `UICollectionView`). Alinee **Inicial**, **Final** e **Inferior** con respecto a **Área segura**. Alinee también **Superior** con respecto a **Contenedor de controles**.

![\[Personalice la Vista de colección de participantes de iOS.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_iOS_2.png)


La segunda vista es **Contenedor de controles**. Alinee **Inicial**, **Final** y **Superior** con respecto a **Área segura**:

![\[Personalice la vista Contenedor de controles de iOS.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_iOS_3.png)


La tercera y última vista es **Vista de pila vertical**. Alinee **Superior**, **Inicial**, **Final** e **Inferior** con respecto a **Supervista**. En cuanto al diseño, establezca el espaciado en 8 en lugar de en 0.

![\[Personalice la vista de pila vertical de iOS.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_iOS_4.png)


Las **UIStackViews** se encargarán del diseño de las vistas restantes. En las tres **UIStackViews**, seleccione la opción **Relleno** para **Alineación** y **Distribución**.

![\[Personalice el resto de las vistas de iOS con UIStackViews.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_iOS_5.png)


Por último, vinculemos estas vistas a nuestro `ViewController`. Desde arriba, asigne las siguientes vistas:
+ El **campo de texto de unión** se asigna a `textFieldToken`.
+ El **botón de unión** se asigna a `buttonJoin`.
+ La **etiqueta de estado** se asigna a `labelState`.
+ El **botón de publicación** se asigna a `switchPublish`.
+ **Vista de colección de participantes** se asigna a `collectionViewParticipants`.

Aproveche también para establecer el `dataSource` del elemento **Vista de colección de participantes** en el `ViewController` propietario:

![\[Configure el dataSource de Vista de colección de participantes de la aplicación de iOS.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_iOS_6.png)


Ahora creamos la subclase `UICollectionViewCell` en la que se renderizarán los participantes. Lo primero es crear un nuevo archivo **Cocoa Touch Class**:

![\[Cree una UICollectionViewCell para renderizar los participantes de iOS en tiempo real.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_iOS_7.png)


Asígnele el nombre `ParticipantUICollectionViewCell` y conviértala en una subclase de `UICollectionViewCell` en Swift. Volvemos al archivo de Swift y creamos `@IBOutlets` que vincular:

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

En el archivo XIB asociado, cree esta jerarquía de vistas:

![\[Cree una jerarquía de vistas de iOS en el archivo XIB asociado.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_iOS_8.png)


En AutoLayout, volveremos a modificar tres vistas. La primera vista es **Vista previa de contenedores**. Alinee **Final**, **Inicial**, **Superior** e **Inferior** con respecto a **Celda de vista de colección de participantes**.

![\[Personalice la Vista previa de contenedores de iOS.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_iOS_9.png)


La segunda vista es **Vista**. Alinee **Inicial** y **Superior** con respecto a **Celda de vista de colección de participantes** y cambie el valor a 4.

![\[Personalice la vista de iOS.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_iOS_10.png)


La tercera vista es **Vista de pila**. Alinee **Final**, **Inicial**, **Superior** e **Inferior** con respecto a **Supervista** y cambie el valor a 4.

![\[Personalice la vista de pila de iOS.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Publish_iOS_11.png)


## Permisos y temporizador de inactividad
<a name="getting-started-pub-sub-ios-perms"></a>

En `ViewController`, tenemos que desactivar el temporizador de inactividad del sistema para evitar que el dispositivo entre en estado de suspensión mientras se esté utilizando nuestra aplicación:

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

A continuación, solicitamos los permisos de cámara y micrófono del sistema:

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

## Estado de la aplicación
<a name="getting-started-pub-sub-ios-app-state"></a>

Tenemos que configurar `collectionViewParticipants` con el archivo de diseño que creamos anteriormente:

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

Para representar a cada participante, creamos una estructura simple llamada `StageParticipant`. Esta puede incluirse en el archivo `ViewController.swift`, aunque también se puede crear un archivo nuevo.

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

Para hacer un seguimiento de esos participantes, los guardamos en una matriz que sea una propiedad privada de `ViewController`:

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

Esta propiedad se utilizará para facilitar información al `UICollectionViewDataSource` que enlazamos en el guion gráfico anterior:

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

}
```

Para ver su propia vista previa antes de unirse a un escenario, creamos inmediatamente un participante local:

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

Esto hace que la celda de un participante se renderice inmediatamente cuando se ejecute la aplicación. La celda representa al participante local.

Los usuarios quieren poder verse antes de unirse a un escenario, por lo que, a continuación, implementamos el método `setupLocalUser()`. El código de gestión de permisos anterior se encarga de llamar a este método. Almacenamos la referencia de la cámara y el micrófono como objetos de `IVSLocalStageStream`.

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

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

Con esto detectamos la cámara y el micrófono del dispositivo a través del SDK y los almacenamos en nuestro objeto `streams` local. A continuación, asignamos la matriz `streams` del primer participante (el participante local que creamos anteriormente) a `streams`. Por último, llamamos a `participantsChanged` con un `index` de 0 y un `changeType` con el valor `updated`. Esta es una función auxiliar para actualizar `UICollectionView` con bonitas animaciones. Este es el resultado:

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

No se preocupe por `cell.set` aún, lo veremos luego, pero ahí es donde renderizaremos el contenido de la celda en función del participante.

`ChangeType` es una enumeración simple:

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

Lo último es ver si el escenario está conectado. Usamos un simple valor `bool` para comprobarlo. Cuando este valor se actualice, actualizará automáticamente nuestra interfaz de usuario.

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

## Implementación del SDK de escenarios
<a name="getting-started-pub-sub-ios-stage-sdk"></a>

Los siguientes tres [conceptos](ios-publish-subscribe.md#ios-publish-subscribe-concepts) básicos subyacen a la funcionalidad de transmisión en tiempo real: escenario, estrategia y renderizador. El objetivo del diseño es minimizar la cantidad de lógica necesaria por parte del cliente para crear un producto que funcione.

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

La implementación de `IVSStageStrategy` es sencilla:

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

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

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

En resumen, solo publicamos si la opción de publicación está activada. Si publicamos, publicaremos las transmisiones que hemos recopilado anteriormente. Por último, en este ejemplo, siempre nos suscribimos a otros participantes y recibimos tanto su contenido de audio como de video.

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

La implementación de `IVSStageRenderer` también es bastante simple, aunque, dada la cantidad de funciones, contiene bastante más código. El enfoque general de este renderizador es actualizar la matriz `participants` cuando el SDK nos notifique un cambio en un participante. Hay ciertos casos en los que tratamos a los participantes locales de forma diferente, porque hemos decidido administrarlos por nuestra cuenta para que puedan ver la vista previa de la cámara antes de unirse.

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

Este código usa una extensión para convertir el estado de conexión en texto legible para nosotros:

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

## Implementación de UICollectionViewLayout personalizado
<a name="getting-started-pub-sub-ios-layout"></a>

Establecer diferentes números de participantes puede resultar complejo. Lo ideal es que ocupen todo el marco de la vista principal, pero gestionar la configuración de cada participante de forma independiente no es la mejor forma de lograrlo. Para facilitarlo, veremos cómo implementar `UICollectionViewLayout`.

Cree otro archivo, `ParticipantCollectionViewLayout.swift`, que debería ampliar `UICollectionViewLayout`. Esta clase usará otra clase llamada `StageLayoutCalculator`, que veremos pronto. La clase recibe los valores de marco calculados para cada participante y, a continuación, genera los objetos de `UICollectionViewLayoutAttributes` necesarios.

```
import Foundation
import UIKit

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

    private let layoutCalculator = StageLayoutCalculator()

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

    override func prepare() {
        super.prepare()

        guard let collectionView = collectionView else { return }

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

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

    override var collectionViewContentSize: CGSize {
        return contentBounds.size
    }

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

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

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

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

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

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

        return attributesArray
    }

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

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

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

La clase `StageLayoutCalculator.swift` es más importante. Esto está pensado para calcular los marcos de cada participante en función del número de participantes en un diseño de filas o columnas basado en flujos. Cada fila tiene la misma altura que las demás, pero las columnas pueden tener diferentes anchuras por fila. Consulte el comentario del código que aparece sobre la variable `layouts` para ver cómo personalizar este comportamiento.

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

}
```

En `Main.storyboard`, asegúrese de establecer la clase que acabamos de crear como la clase de diseño de `UICollectionView`:

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


## Enlace de acciones de la interfaz de usuario
<a name="getting-started-pub-sub-ios-actions"></a>

Ya casi hemos terminado, pero aún tenemos que crear unas cuantas `IBActions`.

Primero, nos encargamos del botón de unión. Responde de manera diferente en función del valor de `connectingOrConnected`. Cuando está conectado, simplemente abandona el escenario. Si está desconectado, lee el texto del token `UITextField` y crea un nuevo `IVSStage` con ese texto. A continuación, agregamos `ViewController` como `strategy`, `errorDelegate` y renderizador para `IVSStage`. Finalmente, nos unimos al escenario de forma asíncrona.

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

La otra acción de la interfaz de usuario que tenemos que enlazar es la opción de publicación:

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

## Renderización de los participantes
<a name="getting-started-pub-sub-ios-participants"></a>

Por último, tenemos que renderizar los datos que recibimos del SDK en la celda de participante que creamos anteriormente. Ya hemos completado la lógica de `UICollectionView`, por lo que solo tenemos que implementar la API `set` en `ParticipantCollectionViewCell.swift`.

Empezamos por agregar la función `empty` y, luego, la analizaremos paso a paso:

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

Primero, analizamos el estado sencillo, el ID del participante, el estado de la publicación y el estado de la suscripción. Para hacerlo, tan solo actualizamos `UILabels` directamente:

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

Las propiedades de texto de las enumeraciones de publicación y suscripción provienen de extensiones locales:

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

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

A continuación, actualizamos los estados silenciados de audio y video. Para obtener los estados silenciados, tenemos que encontrar el `IVSImageDevice` y el `IVSAudioDevice` de la matriz `streams`. Para optimizar el rendimiento, recordaremos los últimos dispositivos conectados.

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

Por último, queremos renderizar una vista previa de `imageDevice` y mostrar las estadísticas de audio de `audioDevice`:

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

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

La última función que tenemos que crear es `updatePreview()`, que agrega una vista previa del participante a nuestra vista:

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

Lo anterior usa una función auxiliar en `UIView` para facilitar la incorporación de subvistas:

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