

# SDK de transmisión de IVS \$1 Transmisión en tiempo real
<a name="broadcast"></a>

El SDK de transmisión para transmisión en tiempo real de Amazon Interactive Video Service (IVS) está pensado para desarrolladores que crean aplicaciones con Amazon IVS. Este SDK está diseñado a fin de aprovechar la arquitectura de Amazon IVS y verá mejoras continuas y nuevas características, junto con Amazon IVS. Como SDK de transmisión nativo, está diseñado para minimizar el impacto en el rendimiento de la aplicación y en los dispositivos con los que los usuarios acceden a la aplicación.

Tenga en cuenta que el SDK de transmisión se usa tanto para enviar como para recibir videos; es decir, utiliza el mismo SDK para los hosts y para los espectadores. No se necesita el SDK de un reproductor independiente.

Su aplicación puede aprovechar las características clave del SDK de transmisión de Amazon IVS:
+ **Transmisión de alta calidad**: el SDK de transmisión admite la transmisión de alta calidad. Reciba video de su cámara y codifíquelo a una velocidad de hasta 720p.
+ **Ajustes automáticos de la velocidad de bits**: los usuarios de smartphones son móviles, por lo que sus condiciones de red pueden cambiar a lo largo de una transmisión. El SDK de transmisión de Amazon IVS ajusta automáticamente la velocidad de bits de video para adaptarse a las condiciones cambiantes de la red.
+ **Soporte vertical y horizontal**: independientemente del modo en que los usuarios mantengan sus dispositivos, la imagen aparece con el lado correcto hacia arriba y se escala según corresponda. El SDK de transmisión admite el tamaño de formato vertical y horizontal. Administra automáticamente la relación de aspecto cuando los usuarios rotan su dispositivo hacia una orientación distinta de la configurada.
+ **Transmisión segura**: las transmisiones de su usuario se cifran mediante TLS, por lo que pueden mantener sus transmisiones seguras.
+ **Dispositivos de audio externos**: el SDK de transmisión de Amazon IVS admite micrófonos externos SCO de audio, USB y Bluetooth.

## Requisitos de la plataforma
<a name="broadcast-platform-requirements"></a>

### Plataformas nativas
<a name="broadcast-native-platforms"></a>


| Plataforma | Versiones compatibles | 
| --- | --- | 
| Android |  9.0\$1: tenga en cuenta que los clientes pueden crear con la versión 6.0 o superiores, pero no podrán utilizar la funcionalidad de transmisión en tiempo real.  | 
| iOS |  14\$1  | 

IVS admite un mínimo de 4 versiones principales de iOS y 6 versiones principales de Android. El soporte de nuestra versión actual se puede extender más allá de estos mínimos. Los clientes recibirán una notificación mediante las notas de la versión del SDK con al menos 3 meses de antelación cuando una versión principal deje de ser compatible.

### Navegadores de escritorio
<a name="browser-desktop"></a>


| Navegador | Plataformas admitidas | Versiones compatibles | 
| --- | --- | --- | 
| Chrome | Windows, macOS | Dos versiones principales (la versión actual y la anterior más reciente) | 
| Firefox | Windows, macOS | Dos versiones principales (la versión actual y la anterior más reciente) | 
| Ubicaciones | Windows 8.1\$1 | Dos versiones principales (la versión actual y la anterior más reciente) No incluye Edge Legacy | 
| Safari | macOS | Dos versiones principales (la versión actual y la anterior más reciente) | 

### Navegadores móviles (iOS y Android)
<a name="browser-mobile"></a>


| Navegador | Plataformas admitidas | Versiones compatibles | 
| --- | --- | --- | 
| Chrome | iOS, Android | Dos versiones principales (la versión actual y la anterior más reciente) | 
| Firefox | Android | Dos versiones principales (la versión actual y la anterior más reciente) | 
| Safari | iOS | Dos versiones principales (la versión actual y la anterior más reciente) | 

#### Limitaciones conocidas
<a name="browser-mobile-limitations"></a>
+ En todos los dispositivos móviles, no recomendamos publicar o suscribirse con más de tres participantes al mismo tiempo, debido a problemas con los artefactos de video y las pantallas negras. Si necesita más publicadores, configure la opción de [publicar y suscribirse solo en audio](web-publish-subscribe.md#web-publish-subscribe-concepts-strategy-updates).
+ No recomendamos componer un escenario y retransmitirlo a un canal de Android Mobile Web por motivos de rendimiento y posibles bloqueos. Si se requiere la funcionalidad de transmisión, integre el [SDK de transmisión para transmisión en tiempo real de IVS para Android](broadcast-android.md).

## Vistas web
<a name="broadcast-webviews"></a>

El SDK de transmisión web no admite vistas web ni entornos similares a los de la web (televisores, consolas, etc.). Para implementaciones móviles, consulte la Guía del SDK de transmisión de transmisión en tiempo para [Android](broadcast-android.md) y para [iOS](broadcast-ios.md).

## Se requiere acceso a los dispositivos
<a name="broadcast-device-access"></a>

El SDK de difusión requiere acceso a las cámaras y micrófonos del dispositivo, tanto los integrados en el dispositivo como los conectados a través de Bluetooth, USB o conector de audio.

## Soporte
<a name="broadcast-support"></a>

El SDK de transmisión se mejora de forma continua. Consulte [Notas de la versión de Amazon IVS](release-notes.md) para ver las versiones disponibles y los problemas solucionados. Si procede, antes de contactar con el soporte técnico, actualice su versión del SDK de transmisión y compruebe si se resuelve el problema.

### Control de versiones
<a name="broadcast-support-versioning"></a>

Los SDK de transmisión de Amazon IVS utilizan el [control de versiones semántico](https://semver.org/).

Para este análisis, suponga:
+ La última versión es la 4.1.3.
+ La última versión de la versión principal anterior es la 3.2.4.
+ La última versión de la versión 1.x es la 1.5.6.

Las características nuevas compatibles con versiones anteriores se agregan como versiones secundarias de la última versión. En este caso, el siguiente conjunto de características nuevas se agregará como la versión 4.2.0.

Se agregan correcciones de errores menores compatibles con versiones anteriores como parches de la última versión. Aquí, el siguiente conjunto de correcciones de errores menores se agregará como la versión 4.1.4.

Las correcciones de errores principales compatibles con versiones anteriores se manejan de manera diferente; estas se agregan a varias versiones:
+ Versión del parche de la última versión. Aquí, esta es la versión 4.1.4.
+ Versión del parche de la versión secundaria anterior. Aquí, esta es la versión 3.2.5.
+ Versión del parche de la última versión 1.x. Aquí, esta es la versión 1.5.7.

El equipo de productos de Amazon IVS define las principales correcciones de errores. Las actualizaciones de seguridad críticas y otras correcciones seleccionadas necesarias para los clientes son ejemplos típicos.

**Nota:** En los ejemplos anteriores, las versiones publicadas aumentan sin omitir ningún número (por ejemplo, de 4.1.3 a 4.1.4). En realidad, uno o más números de parche pueden permanecer internos y no ser lanzados, por lo que la versión publicada podría aumentar de 4.1.3 a 4.1.6.

# SDK de transmisión de IVS: guía para web \$1 Transmisión en tiempo real
<a name="broadcast-web"></a>

El SDK de transmisión web para transmisión en tiempo real de IVS ofrece a los desarrolladores las herramientas para crear experiencias interactivas y en tiempo real en la web. El SDK está pensado para los desarrolladores que crean aplicaciones web con Amazon IVS.

El SDK de transmisión web permite a los participantes enviar y recibir videos. El SDK admite las siguientes operaciones:
+ Incorporación a un escenario
+ Publicación de contenido multimedia para otros participantes del escenario
+ Suscripción al contenido multimedia de otros participantes del escenario
+ Administración y monitoreo del video y audio publicados en el escenario
+ Obtención de estadísticas de WebRTC de cada conexión de pares
+ Todas las operaciones del SDK de transmisión web de transmisión de baja latencia de IVS

**Versión más reciente del SDK de transmisión para web:** 1.33.0 ([notas de la versión](https://docs.aws.amazon.com/ivs/latest/RealTimeUserGuide/release-notes.html#mar12-26-broadcast-web-rt)) 

**Documentación de referencia:** a fin de obtener información sobre los métodos más importantes disponibles en el SDK de transmisión web de Amazon IVS, consulte [https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference). Asegúrese de seleccionar la versión más reciente del SDK.

**Código de ejemplo**: los siguientes ejemplos son un buen punto para empezar rápidamente a usar el SDK:
+ [Reproducción sencilla](https://codepen.io/amazon-ivs/pen/RNwVBRK)
+ [Publicación y suscripción simple](https://codepen.io/amazon-ivs/pen/ZEqgrpo)
+ [Demostración completa de colaboración en tiempo real con React](https://github.com/aws-samples/amazon-ivs-real-time-collaboration-web-demo/tree/main)

**Requisitos de la plataforma**: consulte [SDK de transmisión de Amazon IVS](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/broadcast.html) para obtener un listado de las plataformas compatibles.

**Nota:** Publicar desde un navegador es práctico para los usuarios finales, ya que no requiere la instalación de software adicional. Sin embargo, la publicación basada en un navegador depende de las restricciones y la variabilidad de los entornos de los navegadores. Si necesita priorizar la estabilidad (por ejemplo, para la transmisión de eventos), solemos recomendar publicar desde una fuente que no sea un navegador (por ejemplo, OBS Studio u otros codificadores dedicados), que suelen tener acceso directo a los recursos del sistema y evitan las limitaciones del navegador. Para obtener más información sobre las opciones de publicación distintas del navegador, consulte la documentación de [Ingesta de transmisiones](rt-stream-ingest.md).

# Introducción al SDK de transmisión para web de IVS \$1 Transmisión en tiempo real
<a name="broadcast-web-getting-started"></a>

Este documento explica los pasos necesarios para comenzar a utilizar el SDK de transmisión para web de la transmisión en tiempo real de IVS.

## Importaciones
<a name="broadcast-web-getting-started-imports"></a>

Los componentes básicos de la transmisión en tiempo real se encuentran en un espacio de nombres diferente al de los módulos de transmisión raíz.

### Uso de una etiqueta de script
<a name="broadcast-web-getting-started-imports-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).

Las clases y enumeraciones definidas en los ejemplos siguientes se pueden encontrar en el objeto global `IVSBroadcastClient`:

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

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

Para instalar el paquete de `npm`: 

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

Las clases, enumeraciones y tipos también se pueden importar desde el módulo del paquete:

```
import { Stage, SubscribeType, LocalStageStream } from 'amazon-ivs-web-broadcast'
```

### Compatibilidad con la representación del servidor
<a name="broadcast-web-getting-started-imports-server-side-rendering"></a>

La biblioteca de fases del SDK de transmisión para web no se puede cargar en un contexto del servidor, ya que hace referencia a los elementos básicos del navegador necesarios para el funcionamiento de la biblioteca cuando se carga. Para solucionar este problema, cargue la biblioteca de forma dinámica, como se muestra en la [demostración de transmisión web con Next y React](https://github.com/aws-samples/amazon-ivs-broadcast-web-demo/blob/main/hooks/useBroadcastSDK.js#L26-L31).

## Solicitar permisos
<a name="broadcast-web-request-permissions"></a>

La aplicación debe solicitar permiso para acceder a la cámara y al micrófono del usuario, y deberán ser ofrecidos mediante HTTPS. (Esto no es específico de Amazon IVS; es necesario para cualquier sitio web que necesite acceso a cámaras y micrófonos).

A continuación, le presentamos una función a modo de ejemplo que muestra cómo solicitar y capturar permisos para los dispositivos de audio y video:

```
async function handlePermissions() {
   let permissions = {
       audio: false,
       video: false,
   };
   try {
       const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
       for (const track of stream.getTracks()) {
           track.stop();
       }
       permissions = { video: true, audio: true };
   } catch (err) {
       permissions = { video: false, audio: false };
       console.error(err.message);
   }
   // If we still don't have permissions after requesting them display the error message
   if (!permissions.video) {
       console.error('Failed to get video permissions.');
   } else if (!permissions.audio) {
       console.error('Failed to get audio permissions.');
   }
}
```

Para obtener más información, consulte la [API de permisos](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API) y [MediaDevices.getUserMedia()](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia).

## Enlistar los dispositivos disponibles
<a name="broadcast-web-request-list-devices"></a>

Para conocer los dispositivos que puede capturar, consulte el método [MediaDevices.enumerateDevices()](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices) del navegador:

```
const devices = await navigator.mediaDevices.enumerateDevices();
window.videoDevices = devices.filter((d) => d.kind === 'videoinput');
window.audioDevices = devices.filter((d) => d.kind === 'audioinput');
```

## Recuperar MediaStream de un dispositivo
<a name="broadcast-web-retrieve-mediastream"></a>

Después de obtener la lista de dispositivos disponibles, puede recuperar una transmisión de diversos dispositivos. Por ejemplo, puede utilizar el método `getUserMedia()` para recuperar la transmisión de una cámara.

Si quisiera indicar desde cuál dispositivo desea capturar la transmisión, puede establecer de forma expresa `deviceId` en las secciones `audio` o `video` de las restricciones de multimedia. De forma alternativa, puede omitir `deviceId` y que los usuarios seleccionen los dispositivos desde el símbolo del navegador.

También puede especificar la resolución de cámara ideal mediante las restricciones `width` y `height`. (Obtenga más información sobre estas restricciones [aquí](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#properties_of_video_tracks)). El SDK aplica de forma automática las restricciones máximas de ancho y alto que corresponden a la resolución máxima de transmisión; sin embargo, es aconsejable que las aplique usted mismo para garantizar que la relación de aspecto de la fuente no cambie después de que la agregue al SDK.

Para la transmisión en tiempo real, asegúrese de que el contenido multimedia esté limitado a una resolución de 720p. En concreto, sus valores de ancho y alto de la restricción de `getUserMedia` y `getDisplayMedia` no deben ser superiores a 921 600 (1280\$1720) cuando se multiplican. 

```
const videoConfiguration = {
  maxWidth: 1280,
  maxHeight: 720,
  maxFramerate: 30,
}

window.cameraStream = await navigator.mediaDevices.getUserMedia({
   video: {
       deviceId: window.videoDevices[0].deviceId,
       width: {
           ideal: videoConfiguration.maxWidth,
       },
       height: {
           ideal:videoConfiguration.maxHeight,
       },
   },
});
window.microphoneStream = await navigator.mediaDevices.getUserMedia({
   audio: { deviceId: window.audioDevices[0].deviceId },
});
```

# Publicación y suscripción con el SDK de transmisión para web de IVS \$1 Transmisión en tiempo real
<a name="web-publish-subscribe"></a>

Este documento explica los pasos necesarios para publicar y suscribirse a una fase mediante el SDK de transmisión para web de la transmisión en tiempo real de IVS.

## Conceptos
<a name="web-publish-subscribe-concepts"></a>

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

### Etapa
<a name="web-publish-subscribe-concepts-stage"></a>

La clase `Stage` es el principal punto de interacción entre la aplicación host y el SDK. Representa el escenario como tal y se usa para entrar y salir de él. Para crear un escenario e incorporarse a él, es necesaria una cadena de símbolos válida y que no haya vencido del plano de control (representada como `token`). Entrar y salir de un escenario es sencillo:

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

try {
   await stage.join();
} catch (error) {
   // handle join exception
}

stage.leave();
```

### Strategy (Estrategia)
<a name="web-publish-subscribe-concepts-strategy"></a>

La interfaz `StageStrategy` proporciona una forma para que la aplicación host comunique el estado deseado del escenario al SDK. Es necesario implementar las siguientes tres funciones: `shouldSubscribeToParticipant`, `shouldPublishParticipant` y `stageStreamsToPublish`. Todas se analizan a continuación.

Para usar una estrategia definida, pásela al constructor de `Stage`. El siguiente es un ejemplo completo de una aplicación que utiliza una estrategia para publicar la cámara web de un participante en el escenario y suscribirse a todos los participantes. El propósito de cada función de estrategia necesaria se explica detalladamente en las siguientes secciones.

```
const devices = await navigator.mediaDevices.getUserMedia({ 
   audio: true,
   video: {
        width: { max: 1280 },
        height: { max: 720 },
    } 
});
const myAudioTrack = new LocalStageStream(devices.getAudioTracks()[0]);
const myVideoTrack = new LocalStageStream(devices.getVideoTracks()[0]);

// Define the stage strategy, implementing required functions
const strategy = {
   audioTrack: myAudioTrack,
   videoTrack: myVideoTrack,

   // optional
   updateTracks(newAudioTrack, newVideoTrack) {
      this.audioTrack = newAudioTrack;
      this.videoTrack = newVideoTrack;
   },

   // required
   stageStreamsToPublish() {
      return [this.audioTrack, this.videoTrack];
   },

   // required
   shouldPublishParticipant(participant) {
      return true;
   },

   // required
   shouldSubscribeToParticipant(participant) {
      return SubscribeType.AUDIO_VIDEO;
   }
};

// Initialize the stage and start publishing
const stage = new Stage(token, strategy);
await stage.join();


// To update later (e.g. in an onClick event handler)
strategy.updateTracks(myNewAudioTrack, myNewVideoTrack);
stage.refreshStrategy();
```

#### Suscripción a participantes
<a name="web-publish-subscribe-concepts-strategy-participants"></a>

```
shouldSubscribeToParticipant(participant: StageParticipantInfo): SubscribeType
```

Cuando un participante remoto se incorpora al escenario, el SDK consulta la aplicación host sobre el estado de suscripción deseado de ese participante. Las opciones son `NONE`, `AUDIO_ONLY` y `AUDIO_VIDEO`. Al devolver un valor para esta función, la aplicación host no tiene que preocuparse por el estado de la publicación, el estado actual de la suscripción ni el estado de la conexión del escenario. Si se devuelve `AUDIO_VIDEO`, el SDK espera a que el participante remoto publique antes de suscribirse y actualiza la aplicación host al emitir eventos durante todo el proceso.

Este es un ejemplo de implementación:

```
const strategy = {
   
   shouldSubscribeToParticipant: (participant) => {
      return SubscribeType.AUDIO_VIDEO;
   }

   // ... other strategy functions
}
```

Esta es la implementación completa de esta función para una aplicación host que siempre quiere que todos los participantes se vean entre sí; por ejemplo, una aplicación de videochat.

También son posibles implementaciones más avanzadas. Por ejemplo, supongamos que la aplicación proporciona un atributo `role` al crear el token con CreateParticipantToken. La aplicación podría utilizar la propiedad `attributes` en `StageParticipantInfo` para suscribirse de forma selectiva a los participantes en función de los atributos proporcionados por el servidor:

```
const strategy = {
   
   shouldSubscribeToParticipant(participant) {
      switch (participant.attributes.role) {
         case 'moderator':
            return SubscribeType.NONE;
         case 'guest':
            return SubscribeType.AUDIO_VIDEO;
         default:
            return SubscribeType.NONE;
      }
   }
   // . . . other strategies properties
}
```

Esto se puede utilizar para crear un escenario en el que los moderadores puedan monitorear a todos los invitados sin que ellos mismos los vean ni los escuchen. La aplicación host podría utilizar una lógica empresarial adicional para permitir que los moderadores se vean entre sí, pero permanezcan invisibles para los invitados.

#### Configuración de la suscripción a participantes
<a name="web-publish-subscribe-concepts-strategy-participants-config"></a>

```
subscribeConfiguration(participant: StageParticipantInfo): SubscribeConfiguration
```

Si se está subscribiendo a un participante remoto (consulte [Suscripción a participantes](#web-publish-subscribe-concepts-strategy-participants)), el SDK consulta a la aplicación host sobre una configuración de la subscrición personalizada para ese participante. Esta configuración es opcional y permite a la aplicación host controlar determinados aspectos del comportamiento de los suscriptores. Para obtener información sobre lo que se puede configurar, consulte [SubscribeConfiguration](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/interfaces/SubscribeConfiguration) en la documentación de referencia del SDK.

Este es un ejemplo de implementación:

```
const strategy = {
   
   subscribeConfiguration: (participant) => {
      return {
         jitterBuffer: {
            minDelay: JitterBufferMinDelay.MEDIUM
         }  
      }

   // ... other strategy functions
}
```

Esta implementación actualiza el retraso mínimo del búfer de fluctuación para todos los participantes suscritos al valor preestablecido `MEDIUM`.

Como en el caso de `shouldSubscribeToParticipant`, también son posibles implementaciones más avanzadas. El valor de `ParticipantInfo` proporcionado se puede utilizar para actualizar selectivamente la configuración de suscripción para participantes específicos.

Se recomienda utilizar los comportamientos predeterminados. Especifique la configuración personalizada solo si hay un comportamiento en particular que desee cambiar.

#### Publicación
<a name="web-publish-subscribe-concepts-strategy-publishing"></a>

```
shouldPublishParticipant(participant: StageParticipantInfo): boolean
```

Una vez realizada la conexión al escenario, el SDK consulta la aplicación host para ver si un participante en particular tiene que publicar. Esto solo se invoca en los participantes locales que tienen permiso para publicar en función del token proporcionado.

Este es un ejemplo de implementación:

```
const strategy = {
   
   shouldPublishParticipant: (participant) => {
      return true;
   }

   // . . . other strategies properties
}
```

Esto es para una aplicación de videochat estándar en la que los usuarios siempre quieren publicar. Pueden activar y desactivar el sonido y el video para ocultarse o verse y escucharse al instante. (También pueden usar la opción de publicar o anular la publicación, pero esto es mucho más lento. Es preferible silenciar o activar el sonido en casos de uso en los que se quiera cambiar la visibilidad de manera frecuente).

#### Elección de las transmisiones que publicar
<a name="web-publish-subscribe-concepts-strategy-streams"></a>

```
stageStreamsToPublish(): LocalStageStream[];
```

Al publicar, esto se utiliza para determinar qué transmisiones de audio y video se tienen que publicar. Esto se explica en mayor detalle más adelante en [Publicación de una transmisión multimedia](#web-publish-subscribe-publish-stream).

#### Actualización de la estrategia
<a name="web-publish-subscribe-concepts-strategy-updates"></a>

La estrategia pretende ser dinámica: los valores devueltos por cualquiera de las funciones anteriores se pueden cambiar en cualquier momento. Por ejemplo, si la aplicación host no quiere publicar hasta que el usuario final presione un botón, puede devolver una variable de `shouldPublishParticipant` (como `hasUserTappedPublishButton`). Cuando esa variable cambie en función de una interacción del usuario final, llame a `stage.refreshStrategy()` para indicar al SDK que debe consultar la estrategia a fin de obtener los valores más recientes y aplicar solo los cambios. Si el SDK observa que el valor `shouldPublishParticipant` cambió, se iniciará el proceso de publicación. Si las consultas del SDK y todas las funciones devuelven el mismo valor que antes, la llamada a `refreshStrategy` no hará cambios en el escenario.

Si el valor devuelto de `shouldSubscribeToParticipant` cambia de `AUDIO_VIDEO` a `AUDIO_ONLY`, la transmisión de video se elimina para todos los participantes con valores devueltos modificados, si ya existía con anterioridad una transmisión de video.

Por lo general, el escenario utiliza la estrategia para aplicar de la manera más eficiente la diferencia entre las estrategias anteriores y actuales, sin que la aplicación host tenga que preocuparse por todo el estado necesario para administrarla correctamente. Por eso, piense en la llamada a `stage.refreshStrategy()` como una operación barata, porque no hace nada a no ser que cambie la estrategia.

### Events
<a name="web-publish-subscribe-concepts-events"></a>

Una instancia `Stage` es un emisor de eventos. Con `stage.on()`, el estado del escenario se comunica a la aplicación host. Por lo general, los eventos permiten actualizar por completo la interfaz de usuario de la aplicación host. Los eventos son los siguientes:

```
stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_PUBLISH_STATE_CHANGED, (participant, state) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_SUBSCRIBE_STATE_CHANGED, (participant, state) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_REMOVED, (participant, streams) => {})
stage.on(StageEvents.STAGE_STREAM_ADAPTION_CHANGED, (participant, stream, isAdapting) => ())
stage.on(StageEvents.STAGE_STREAM_LAYERS_CHANGED, (participant, stream, layers) => ())
stage.on(StageEvents.STAGE_STREAM_LAYER_SELECTED, (participant, stream, layer, reason) => ())
stage.on(StageEvents.STAGE_STREAM_MUTE_CHANGED, (participant, stream) => {})
stage.on(StageEvents.STAGE_STREAM_SEI_MESSAGE_RECEIVED, (participant, stream) => {})
```

Para la mayoría de estos métodos, se proporciona la `ParticipantInfo` correspondiente.

No se espera que la información que proporcionan los eventos afecte a los valores de retorno de la estrategia. Por ejemplo, no se espera que el valor devuelto de `shouldSubscribeToParticipant` cambie cuando se llama a `STAGE_PARTICIPANT_PUBLISH_STATE_CHANGED`. Si la aplicación host quiere suscribirse a un participante en particular, debe devolver el tipo de suscripción deseado, independientemente del estado de publicación de ese participante. El SDK es responsable de garantizar que se aplique el estado deseado de la estrategia en el momento correcto según el estado del escenario.

## Publicación de una transmisión multimedia
<a name="web-publish-subscribe-publish-stream"></a>

Los dispositivos locales, como micrófonos y cámaras, se recuperan siguiendo los mismos pasos descritos anteriormente en [Recuperación de MediaStream de un dispositivo](broadcast-web-getting-started.md#broadcast-web-retrieve-mediastream). En el ejemplo, utilizamos `MediaStream` para crear una lista de objetos `LocalStageStream` que el SDK utiliza para publicar:

```
try {
    // Get stream using steps outlined in document above
    const stream = await getMediaStreamFromDevice();

    let streamsToPublish = stream.getTracks().map(track => {
        new LocalStageStream(track)
    });

    // Create stage with strategy, or update existing strategy
    const strategy = {
        stageStreamsToPublish: () => streamsToPublish
    }
}
```

## Publicación de una pantalla compartida
<a name="web-publish-subscribe-publish-screenshare"></a>

Las aplicaciones suelen necesitar publicar una pantalla compartida además de la cámara web del usuario. Para publicar una pantalla compartida, es necesario crear un token adicional para la fase, específicamente para publicar el contenido multimedia de la pantalla compartida. Use `getDisplayMedia` y restrinja la resolución a un máximo de 720p. Después de eso, los pasos son similares a publicar una cámara en la fase.

```
// Invoke the following lines to get the screenshare's tracks
const media = await navigator.mediaDevices.getDisplayMedia({
   video: {
      width: {
         max: 1280,
      },
      height: {
         max: 720,
      }
   }
});
const screenshare = { videoStream: new LocalStageStream(media.getVideoTracks()[0]) };
const screenshareStrategy = {
   stageStreamsToPublish: () => {
      return [screenshare.videoStream];
   },
   shouldPublishParticipant: (participant) => {
      return true;
   },
   shouldSubscribeToParticipant: (participant) => {
      return SubscribeType.AUDIO_VIDEO;
   }
}
const screenshareStage = new Stage(screenshareToken, screenshareStrategy);
await screenshareStage.join();
```

## Visualización y eliminación de participantes
<a name="web-publish-subscribe-participants"></a>

Cuando se complete la suscripción, recibirá una matriz de objetos `StageStream` a través del evento `STAGE_PARTICIPANT_STREAMS_ADDED`. El evento también le brinda información sobre los participantes que le será de ayuda al visualizar las transmisiones multimedia:

```
stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_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)
    }

    // Create or find video element already available in your application
    const videoEl = getParticipantVideoElement(participant.id);

    // Attach the participants streams
    videoEl.srcObject = new MediaStream();
    streamsToDisplay.forEach(stream => videoEl.srcObject.addTrack(stream.mediaStreamTrack));
})
```

Cuando un participante deja de publicar o anula la suscripción de una transmisión, se invoca la función `STAGE_PARTICIPANT_STREAMS_REMOVED` con las transmisiones que se eliminaron. Las aplicaciones host tienen que usar esto como una señal para eliminar la transmisión de video del participante de DOM.

`STAGE_PARTICIPANT_STREAMS_REMOVED` se invoca para todas las situaciones en las que se puede eliminar una transmisión, como las siguientes:
+ El participante remoto deja de publicar.
+ Un dispositivo local cancela la suscripción o cambia la suscripción de `AUDIO_VIDEO` a `AUDIO_ONLY`.
+ El participante remoto abandona el escenario.
+ El participante local abandona el escenario.

Ya que `STAGE_PARTICIPANT_STREAMS_REMOVED` se invoca en todas las situaciones, no es necesaria una lógica empresarial personalizada para eliminar a los participantes de la interfaz de usuario durante las operaciones de licencia remota o local.

## Activación y desactivación del sonido de las transmisiones multimedia
<a name="web-publish-subscribe-mute-streams"></a>

Los objetos de `LocalStageStream` tienen una función `setMuted` que controla si la transmisión está silenciada. Esta función se puede invocar en la transmisión antes o después de que la devuelva la función de estrategia `stageStreamsToPublish`.

**Importante**: Si `stageStreamsToPublish` devuelve una nueva instancia de objeto de `LocalStageStream` después de una llamada a `refreshStrategy`, el estado de sonido desactivado del nuevo objeto de transmisión se aplicará al escenario. Tenga cuidado al crear nuevas instancias `LocalStageStream` para asegurarse de que se mantenga el estado de sonido desactivado que se espera.

## Monitoreo del estado de sonido desactivado en los contenidos multimedia del participante remoto
<a name="web-publish-subscribe-mute-state"></a>

Cuando los participantes cambian el estado de sonido desactivado de su video o audio, el evento `STAGE_STREAM_MUTE_CHANGED` se activa con una lista de las transmisiones que cambiaron. Use la propiedad `isMuted` en `StageStream` para actualizar su interfaz de usuario según corresponda:

```
stage.on(StageEvents.STAGE_STREAM_MUTE_CHANGED, (participant, stream) => {
   if (stream.streamType === 'video' && stream.isMuted) {
       // handle UI changes for video track getting muted
   }
})
```

Además, puede consultar [StageParticipantInfo](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference#stageparticipantinfo) para obtener información del estado sobre si el audio o el video están silenciados:

```
stage.on(StageEvents.STAGE_STREAM_MUTE_CHANGED, (participant, stream) => {
   if (participant.videoStopped || participant.audioMuted) {
       // handle UI changes for either video or audio
   }
})
```

## Obtención de estadísticas de WebRTC
<a name="web-publish-subscribe-webrtc-stats"></a>

El método `requestQualityStats()` proporciona acceso a estadísticas WebRTC detalladas para transmisiones locales y remotas. Está disponible en los objetos LocalStageStream y RemoteStageStream. Devuelve métricas de calidad completas, como la calidad de la red, las estadísticas de paquetes, la información sobre la velocidad de bits y las métricas relacionadas con las tramas.

Este es un método asíncrono con el que puede recuperar estadísticas mediante await o encadenando una promesa. Se devuelve `undefined` cuando las estadísticas no están disponibles; por ejemplo, si la transmisión no está activa o las estadísticas internas no están disponibles. Si hay estadísticas disponibles y en función de la transmisión (remota o local, video o audio), el método devuelve un objeto [LocalVideoStats](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/interfaces/LocalVideoStats), [LocalAudioStats](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/interfaces/LocalAudioStats), [RemoteVideoStats](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/interfaces/RemoteVideoStats) o [RemoteAudioStats](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/interfaces/RemoteAudioStats).

Tenga en cuenta que, en el caso de las transmisiones de vídeo con transmisión simultánea, la matriz contiene varios objetos de estadísticas (uno por capa).

**Prácticas recomendadas**
+ Frecuencia de sondeo: llame a `requestQualityStats()` intervalos razonables (de 1 a 5 segundos) para evitar que el rendimiento se vea afectado
+ Gestión de errores: compruebe siempre si el valor devuelto es `undefined` antes de procesarlo
+ Administración de memoria: borre los intervalos y tiempos de espera cuando las transmisiones ya no sean necesarias
+ Calidad de la red: se utiliza `networkQuality` para obtener comentarios de los usuarios sobre las posibles degradaciones causadas por la red. Para obtener más información, consulte [NetworkQuality](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/enumerations/NetworkQuality).

**Ejemplo de uso**

```
// For local streams
const localStats = await localVideoStream.requestQualityStats();
const audioStats = await localAudioStream.requestQualityStats();

// For remote streams
const remoteVideoStats = await remoteVideoStream.requestQualityStats();
const remoteAudioStats = await remoteAudioStream.requestQualityStats();

// Example: Monitor stats every 10 seconds
const statsInterval = setInterval(async () => {
   const stats = await localVideoStream.requestQualityStats();
   if (stats) {
      // Note: If simulcast is enabled, you may receive multiple 
      // stats records for each layer
      stats.forEach(layer => {
         const rid = layer.rid || 'default';
         console.log(`Layer ${rid}:`, {
            active: layer.active,
            networkQuality: layer.networkQuality,
            packetsSent: layer.packetsSent,
            bytesSent: layer.bytesSent,
            resolution: `${layer.frameWidth}x${layer.frameHeight}`,
            fps: layer.framesPerSecond
         });
      });
   }
}, 10000);
```

## Optimización del contenido multimedia
<a name="web-publish-subscribe-optimizing-media"></a>

Se recomienda limitar las llamadas a `getUserMedia` y a `getDisplayMedia` con las siguientes restricciones para obtener el mejor rendimiento:

```
const CONSTRAINTS = {
    video: {
        width: { ideal: 1280 }, // Note: flip width and height values if portrait is desired
        height: { ideal: 720 },
        framerate: { ideal: 30 },
    },
};
```

Puede restringir aún más el contenido multimedia mediante opciones adicionales que se pasan al constructor `LocalStageStream`:

```
const localStreamOptions = {
    minBitrate?: number;
    maxBitrate?: number;
    maxFramerate?: number;
    simulcast: {
        enabled: boolean
    }
}
const localStream = new LocalStageStream(track, localStreamOptions)
```

En el código anterior:
+ `minBitrate` establece la velocidad de bits mínima que se espera que utilice el navegador. Sin embargo, una transmisión de video de poca complejidad puede hacer que el codificador establezca una velocidad inferior a esta velocidad de bits.
+ `maxBitrate` establece una velocidad de bits máxima que se espera que el navegador no supere para esta transmisión.
+ `maxFramerate` establece una velocidad de fotogramas máxima que se espera que el navegador no supere para esta transmisión.
+ La opción `simulcast` solo se puede utilizar en navegadores basados en Chromium. Permite enviar tres capas de representación de la transmisión.
  + Esto permite al servidor elegir qué representación enviar a otros participantes, en función de sus limitaciones de red.
  + Cuando `simulcast` se especifica junto con un valor de `maxBitrate` o `maxFramerate`, se espera que la capa de representación más alta se configure teniendo en cuenta estos valores, siempre que `maxBitrate` no sea inferior al valor predeterminado de `maxBitrate` (900 kbps) de la segunda capa más alta del SDK interno.
  + Si `maxBitrate` se especifica como demasiado baja en comparación con el valor predeterminado de la segunda capa más alta, `simulcast` se desactivará.
  + `simulcast` no se puede activar y desactivar sin volver a publicar el contenido multimedia. Para ello, `shouldPublishParticipant` tiene que devolver `false` y llamar a `refreshStrategy`; `shouldPublishParticipant` tiene que devolver `true` y llamar a `refreshStrategy` otra vez.

## Obtención de los atributos de los participantes
<a name="web-publish-subscribe-participant-attributes"></a>

Si especifica atributos en la solicitud de la operación de `CreateParticipantToken`, podrá ver los atributos en las propiedades de `StageParticipantInfo`:

```
stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {
   console.log(`Participant ${participant.id} info:`, participant.attributes);
})
```

## Información de mejora adicional (SEI)
<a name="web-publish-subscribe-sei-attributes"></a>

La unidad NAL de información de mejora adicional (SEI) se usa para almacenar metadatos alineados con los fotogramas junto con el video. Se puede utilizar al publicar y suscribirse a transmisiones de video H.264. No se garantiza que las cargas útiles de SEI lleguen a los suscriptores, sobre todo en malas condiciones de la red. Como la carga útil del SEI almacena los datos directamente dentro de la estructura de fotogramas H.264, esta capacidad no se puede aprovechar para transmisiones únicamente de audio.

### Inserción de cargas útiles de SEI
<a name="sei-attributes-inserting-sei-payloads"></a>

Los clientes de publicación pueden insertar cargas útiles de SEI en una transmisión de escenario que se esté publicando si se configura el LocalStageStream del video para activar `inBandMessaging` y, posteriormente, se invoca el método `insertSeiMessage`. Tenga en cuenta que habilitar `inBandMessaging` aumenta el uso de memoria del SDK.

Las cargas útiles deben ser del tipo [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). El tamaño de la carga útil debe ser superior a 0 KB e inferior a 1 KB. El número de mensajes SEI insertados por segundo no debe superar los 10 KB por segundo.

```
const config = {
    inBandMessaging: { enabled: true }
};
const vidStream = new LocalStageStream(videoTrack, config);
const payload = new TextEncoder().encode('hello world').buffer;
vidStream.insertSeiMessage(payload);
```

#### Cargas útiles de SEI que se repiten
<a name="sei-attributes-repeating-sei-payloads"></a>

De manera opcional, proporcione un `repeatCount` para repetir la inserción de las cargas útiles del SEI para los siguientes N fotogramas enviados. Esto podría ser útil para reducir la pérdida inherente que puede producirse debido al protocolo de transporte UDP subyacente utilizado para enviar el video. Tenga en cuenta que este valor debe estar entre 0 y 30. Los clientes receptores deben tener una lógica para desduplicar el mensaje.

```
vidStream.insertSeiMessage(payload, { repeatCount: 5 }); // Optional config, repeatCount must be between 0 and 30
```

### Lectura de cargas útiles de SEI
<a name="sei-attributes-reading-sei-payloads"></a>

Los clientes suscritos pueden leer las cargas útiles de SEI de un publicador que publique video H.264, si está presente, al configurar el `SubscribeConfiguration` de los suscriptores para que habiliten `inBandMessaging` y escuchen el evento `StageEvents.STAGE_STREAM_SEI_MESSAGE_RECEIVED`, como se muestra en el siguiente ejemplo:

```
const strategy = {
    subscribeConfiguration: (participant) => {
        return {
            inBandMessaging: {
                enabled: true
            }
        }
    }
    // ... other strategy functions
}

stage.on(StageEvents.STAGE_STREAM_SEI_MESSAGE_RECEIVED, (participant, seiMessage) => {
    console.log(seiMessage.payload, seiMessage.uuid);
});
```

## Codificación por capas con la transmisión simultánea
<a name="web-publish-subscribe-layered-encoding-simulcast"></a>

La codificación por capas con transmisión simultánea es una característica de transmisión en tiempo real de IVS que permite a quienes publican enviar múltiples capas de video con diferentes niveles de calidad, mientras que los suscriptores pueden cambiar estas capas de manera dinámica o manual. Esta característica se describe con más detalle en el documento [Optimizaciones de transmisión](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/real-time-streaming-optimization.html).

### Configuración de la codificación por capas (publicador)
<a name="web-layered-encoding-simulcast-configure-publisher"></a>

Como publicador, para habilitar la codificación por capas con transmisión simultánea, agregue la siguiente configuración a `LocalStageStream` en la instanciación:

```
// Enable Simulcast
let cameraStream = new LocalStageStream(cameraDevice, {
   simulcast: { enabled: true }
})
```

En función de la resolución de entrada del dispositivo de cámara, se codificará y enviará una cantidad determinada de capas, tal como se define en la sección [Capas, calidades y velocidades de fotogramas predeterminadas](real-time-streaming-optimization.md#real-time-streaming-optimization-default-layers) de *Optimizaciones de transmisión*.

Además, si lo desea, puede configurar capas individuales desde la configuración de transmisión simultánea:

```
import { SimulcastLayerPresets } from ‘amazon-ivs-web-broadcast’

// Enable Simulcast
let cameraStream = new LocalStageStream(cameraDevice, {
   simulcast: {
      enabled: true,
      layers: [
         SimulcastLayerPresets.DEFAULT_720,
          SimulcastLayerPresets.DEFAULT_360,
          SimulcastLayerPresets.DEFAULT_180, 
   }
})
```

De manera opcional, también puede crear sus propias configuraciones de capas personalizadas para hasta tres capas. Si proporciona una matriz vacía o ningún valor, se utilizan los valores predeterminados descritos con anterioridad. Las capas se describen con las siguientes propiedades obligatorias:
+ `height: number;`
+ `width: number;`
+ `maxBitrateKbps: number;`
+ `maxFramerate: number;`

A partir de los ajustes preestablecidos, puede anular las propiedades individuales o crear una configuración completamente nueva:

```
import { SimulcastLayerPresets } from ‘amazon-ivs-web-broadcast’

const custom720pLayer = {
   ...SimulcastLayerPresets.DEFAULT_720,
   maxFramerate: 15,
}

const custom360pLayer = {
       maxBitrateKbps: 600,
       maxFramerate: 15,
       width: 640,
       height: 360,
}

// Enable Simulcast
let cameraStream = new LocalStageStream(cameraDevice, {
   simulcast: {
      enabled: true,
      layers: [
         custom720pLayer,
         custom360pLayer, 
   }
})
```

Para conocer los valores máximos, los límites y los errores que se pueden activar al configurar capas individuales, consulta la documentación de referencia del SDK.

### Configuración de la codificación por capas (suscriptor)
<a name="web-layered-encoding-simulcast-configure-subscriber"></a>

Como suscriptor, no es necesario hacer nada para habilitar la codificación por capas. Si un publicador envía capas de transmisión simultánea, el servidor, de forma predeterminada, se adaptará dinámicamente entre las capas para elegir la calidad óptima en función de las condiciones de la red y del dispositivo del suscriptor.

Como alternativa, existen varias opciones para seleccionar las capas explícitas que envía el publicador, tal y como se describe a continuación.

### Opción 1: preferencia de calidad de la capa inicial
<a name="web-layered-encoding-simulcast-layer-quality-preference"></a>

Mediante la estrategia `subscribeConfiguration`, es posible elegir qué capa inicial desea recibir como suscriptor:

```
const strategy = {
    subscribeConfiguration: (participant) => {
        return {
            simulcast: {
                initialLayerPreference: InitialLayerPreference.LOWEST_QUALITY
            }
        }
    }
    // ... other strategy functions
}
```

De forma predeterminada, a los suscriptores siempre se les envía primero la capa de calidad más baja; poco a poco se incrementa hasta llegar a la capa de calidad más alta. Esto optimiza el consumo de ancho de banda del usuario final y proporciona el mejor tiempo de inicio del video, lo cual reduce las congelaciones iniciales de video para los usuarios en redes más débiles.

Estas opciones se encuentran disponibles para `InitialLayerPreference`:
+ `LOWEST_QUALITY`: el servidor entrega primero la capa de video de menor calidad. Esto optimiza tanto el consumo de ancho de banda como el tiempo de inicio del contenido multimedia. La calidad se define como la combinación de tamaño, velocidad de bits y velocidad de fotogramas del video. Por ejemplo, un video 720p es de menor calidad que un video 1080p.
+ `HIGHEST_QUALITY`: el servidor entrega primero la capa de video de mayor calidad. Esto optimiza la calidad, pero puede aumentar el tiempo de inicio del contenido multimedia. La calidad se define como la combinación de tamaño, velocidad de bits y velocidad de fotogramas del video. Por ejemplo, el video 1080p es de mayor calidad que el video 720p.

**Nota:** Para que se apliquen las preferencias de la capa inicial (la llamada `initialLayerPreference`), es necesario volver a suscribirse, ya que estas actualizaciones no se aplican a la suscripción activa.



### Opción 2: capa preferida para la transmisión
<a name="web-layered-encoding-simulcast-preferred-layer"></a>

Una vez iniciada la transmisión, podrá utilizar el método de estrategia `preferredLayerForStream `. Este método de estrategia expone la información del participante y de la transmisión.

El método de estrategia se puede devolver de la siguiente manera:
+ El objeto de capa directamente, basado en lo que `RemoteStageStream.getLayers` devuelve 
+ La cadena de etiqueta del objeto de capa, en función de `StageStreamLayer.label`
+ Sin definir o nulo, lo que indica que no se debe seleccionar ninguna capa y que se prefiere la adaptación dinámica

Por ejemplo, la siguiente estrategia hará que los usuarios seleccionen siempre la capa de video de menor calidad disponible:

```
const strategy = {
    preferredLayerForStream: (participant, stream) => {
        return stream.getLowestQualityLayer();
    }
    // ... other strategy functions
}
```

Para restablecer la selección de capas y volver a la adaptación dinámica, devuelva nulo o sin definir en la estrategia. En este ejemplo, `appState` es una variable ficticia que representa el estado posible de la aplicación.

```
const strategy = {
    preferredLayerForStream: (participant, stream) => {
        if (appState.isAutoMode) {
            return null;
        } else {
            return appState.layerChoice
        }
    }
    // ... other strategy functions
}
```

### Opción 3: ayudantes de la capa RemoteStageStream
<a name="web-layered-encoding-simulcast-remotestagestream-helpers"></a>

`RemoteStageStream` cuenta con varios ayudantes que se pueden utilizar para tomar decisiones sobre la selección de capas y mostrar las selecciones correspondientes a los usuarios finales:
+ **Eventos de capa**: además de los `StageEvents`, el propio objeto `RemoteStageStream` tiene eventos que comunican los cambios de adaptación de capa y transmisión simultánea:
  + `stream.on(RemoteStageStreamEvents.ADAPTION_CHANGED, (isAdapting) => {})`
  + `stream.on(RemoteStageStreamEvents.LAYERS_CHANGED, (layers) => {})`
  + `stream.on(RemoteStageStreamEvents.LAYER_SELECTED, (layer, reason) => {})`
+ **Métodos de capa**: `RemoteStageStream` tiene varios métodos de ayudante que se pueden utilizar para obtener información sobre la transmisión y las capas que se presentan. Estos métodos están disponibles en la transmisión remota proporcionada en la estrategia `preferredLayerForStream `, así como en las transmisiones remotas expuestas a través de `StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED`.
  + `stream.getLayers`
  + `stream.getSelectedLayer`
  + `stream.getLowestQualityLayer`
  + `stream.getHighestQualityLayer`

Para conocer los detalles, consulte la clase `RemoteStageStream` en la [documentación de referencia del SDK](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference). En cuanto a la razón `LAYER_SELECTED`, si se devuelve `UNAVAILABLE`, esto indica que no se ha podido seleccionar la capa solicitada. En su lugar, se selecciona la mejor opción posible, que suele ser una capa de menor calidad para mantener la estabilidad de la transmisión.

## Gestión de los problemas de red
<a name="web-publish-subscribe-network-issues"></a>

Cuando se pierde la conexión de red del dispositivo local, el SDK se intenta volver a conectar internamente sin que el usuario lleve a cabo ninguna acción. En algunos casos, el SDK no funciona de manera correcta y es necesario que el usuario actúe.

En términos generales, el estado del escenario se puede gestionar mediante el evento `STAGE_CONNECTION_STATE_CHANGED`:

```
stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {
   switch (state) {
      case StageConnectionState.DISCONNECTED:
         // handle disconnected UI
         return;
      case StageConnectionState.CONNECTING:
         // handle establishing connection UI
         return;
      case StageConnectionState.CONNECTED:
         // SDK is connected to the Stage
         return;
      case StageConnectionState.ERRORED:
         // SDK encountered an error and lost its connection to the stage. Wait for CONNECTED.
         return;
    }
})
```

En general, puede ignorar un estado de error que se produzca tras unirse correctamente a una fase, ya que el SDK intentará recuperarse internamente. Si el SDK informa de un estado `ERRORED` y la fase permanece en el estado `CONNECTING` durante un periodo prolongado (por ejemplo, 30 segundos o más), es probable que se haya desconectado de la red.

## Transmisión del escenario a un canal de IVS
<a name="web-publish-subscribe-broadcast-stage"></a>

Para transmitir un escenario, cree una sesión de `IVSBroadcastClient` independiente y, a continuación, siga las instrucciones habituales para la transmisión con el SDK, descritas con anterioridad. La lista de `StageStream` expuestas mediante `STAGE_PARTICIPANT_STREAMS_ADDED` se puede utilizar para recuperar las transmisiones multimedia participantes aplicables a la composición del flujo de transmisión, de la siguiente manera:

```
// Setup client with preferred settings
const broadcastClient = getIvsBroadcastClient();

stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => {
    streams.forEach(stream => {
        const inputStream = new MediaStream([stream.mediaStreamTrack]);
        switch (stream.streamType) {
            case StreamType.VIDEO:
                broadcastClient.addVideoInputDevice(inputStream, `video-${participant.id}`, {
                    index: DESIRED_LAYER,
                    width: MAX_WIDTH,
                    height: MAX_HEIGHT
                });
                break;
            case StreamType.AUDIO:
                broadcastClient.addAudioInputDevice(inputStream, `audio-${participant.id}`);
                break;
        }
    })
})
```

Si lo desea, puede componer un escenario y transmitirlo a un canal de IVS de baja latencia para llegar a un público más amplio. Consulte [Habilitación de varios hosts en una transmisión de Amazon IVS](https://docs.aws.amazon.com//ivs/latest/LowLatencyUserGuide/multiple-hosts.html) en la Guía del usuario de transmisión de baja latencia.

# Problemas conocidos y soluciones alternativas del SDK de transmisión para web de IVS \$1 Transmisión en tiempo real
<a name="broadcast-web-known-issues"></a>

Este documento enumera problemas conocidos que puede experimentar al utilizar el SDK de transmisión para web de la transmisión en tiempo real de Amazon IVS y sugiere posibles soluciones alternativas.
+ Al cerrar las pestañas del navegador o salir de los navegadores sin llamar a `stage.leave()`, los usuarios pueden seguir apareciendo en la sesión con una pantalla congelada o negra durante un máximo de 10 segundos.

  **Solución alternativa:** ninguna.
+ Las sesiones de Safari aparecen de forma intermitente con una pantalla negra para que los usuarios se unan una vez iniciada la sesión.

  **Solución alternativa:** actualice el navegador y vuelva a conectar la sesión.
+ Safari no se recupera correctamente al cambiar de red.

  **Solución alternativa:** actualice el navegador y vuelva a conectar la sesión.
+ La consola para desarrolladores repite un error `Error: UnintentionalError at StageSocket.onClose`.

  **Solución alternativa:** solo se puede crear un escenario por token de participante. Este error se produce cuando se crea más de una instancia `Stage` con el mismo token de participante, independientemente de si la instancia está en un dispositivo o en varios.
+ Es posible que tenga problemas para mantener el estado `StageParticipantPublishState.PUBLISHED` y que reciba estados `StageParticipantPublishState.ATTEMPTING_PUBLISH` repetidos al escuchar el evento `StageEvents.STAGE_PARTICIPANT_PUBLISH_STATE_CHANGED`.

  **Solución alternativa:** limite la resolución de video a 720p al invocar a `getUserMedia` o `getDisplayMedia`. En concreto, sus valores de ancho y alto de la restricción de `getUserMedia` y `getDisplayMedia` no deben ser superiores a 921 600 (1280\$1720) cuando se multiplican.
+ Cuando se invoca `stage.leave()` o un participante remoto abandona, aparece un error 404 DELETE en la consola de depuración del navegador.

  **Solución alternativa:** ninguna. Este es un error sin consecuencias.

## Limitaciones de Safari
<a name="broadcast-web-safari-limitations"></a>
+ Para denegar un mensaje de permisos, debe restablecer el permiso en la configuración del sitio web de Safari en el sistema operativo.
+ Safari no detecta todos los dispositivos de forma directa con la misma eficacia que Firefox o Chrome. Por ejemplo, no identifica la cámara virtual OBS.

## Limitaciones de Firefox
<a name="broadcast-web-firefox-limitations"></a>
+ Los permisos del sistema deben estar habilitados para que Firefox pueda compartir la pantalla. Luego de activarlos, el usuario debe reiniciar Firefox para que funcione sin inconvenientes; de lo contrario, si considera que los permisos están bloqueados, el navegador generará una excepción [NotFoundError](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia#exceptions).
+ Falta el método `getCapabilities`. Esto significa que los usuarios no pueden obtener la resolución o la relación de aspecto de la pista multimedia. Consulte este [hilo de Bugzilla](https://bugzilla.mozilla.org/show_bug.cgi?id=1179084).
+ Faltan varias propiedades `AudioContext`, por ejemplo, la latencia y el recuento de canales. Esto podría suponer un problema para los usuarios avanzados que desean manejar las pistas de audio.
+ Las imágenes de la cámara de `getUserMedia` están limitadas a una relación de aspecto 4:3 en MacOS. Consulte el [hilo 1 de Bugzilla](https://bugzilla.mozilla.org/show_bug.cgi?id=1193640) y el [hilo 2 de Bugzilla](https://bugzilla.mozilla.org/show_bug.cgi?id=1306034).
+ Con `getDisplayMedia`, la captura de audio no es compatible. Consulte este [hilo de Bugzilla](https://bugzilla.mozilla.org/show_bug.cgi?id=1541425).
+ La velocidad de fotogramas en la captura de pantalla es poco óptima (¿aproximadamente 15 fps?). Consulte este [hilo de Bugzilla](https://bugzilla.mozilla.org/show_bug.cgi?id=1703522).

## Limitaciones de la web móvil
<a name="broadcast-web-mobile-web-limitations"></a>
+ Los dispositivos móviles no admiten el uso compartido de pantalla con [getDisplayMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia#browser_compatibility).

  **Solución alternativa:** ninguna.
+ El participante tarda entre 15 y 30 segundos en salir cuando cierra un navegador sin llamar a `leave()`.

  **Solución alternativa**: añada una interfaz de usuario que anime a los usuarios a desconectarse correctamente.
+ Poner en segundo plano la aplicación hace que se detenga la publicación del video.

  **Solución alternativa**: muestre una lista de interfaz de usuario cuando el publicador esté en pausa.
+ La velocidad de fotogramas del video se reduce durante aproximadamente 5 segundos después de desactivar el silenciamiento de una cámara en los dispositivos Android.

  **Solución alternativa:** ninguna.
+ La transmisión de video se estira al girar para iOS 16.0.

  **Solución alternativa**: muestre una interfaz de usuario en la que se describa este problema conocido del sistema operativo.
+ Al cambiar el dispositivo de entrada de audio, se cambia automáticamente el dispositivo de salida de audio.

  **Solución alternativa:** ninguna.
+ Al poner en segundo plano el navegador, el flujo de publicación se pone en negro y solo produce audio.

  **Solución alternativa:** ninguna. Lo hacemos por motivos de seguridad.

# Control de errores en el SDK de transmisión para web de IVS \$1 Transmisión en tiempo real
<a name="broadcast-web-error-handling"></a>

En esta sección se ofrece información general sobre las condiciones de error, cómo el SDK de transmisión para web las notifica a la aplicación y qué debe hacer una aplicación cuando se producen esos errores. El SDK informa de los errores a los oyentes del evento `StageEvents.ERROR`:

```
stage.on(StageEvents.ERROR, (error: StageError) => {
    // log or handle errors here
    console.log(`${error.code}, ${error.category}, ${error.message}`);
});
```

## Errores de fases
<a name="web-error-handling-stage-errors"></a>

Se informa de un StageError cuando el SDK encuentra un problema del que no puede recuperarse y, por lo general, requiere la intervención de la aplicación o la reconexión de la red para recuperarse.

Cada `StageError` notificado tiene un código (o `StageErrorCode`), un mensaje (cadena) y una categoría (`StageErrorCategory`). Cada uno está relacionado con una categoría de operación subyacente.

La categoría de operación del error se determina en función de si está relacionado con la conexión a la fase (`JOIN_ERROR`), el envío de contenido multimedia a la fase (`PUBLISH_ERROR`) o la recepción de una transmisión de contenido multimedia entrante desde la fase (`SUBSCRIBE_ERROR`).

La propiedad de código de un `StageError` notifica el problema específico:


| Nombre | Código | Acción recomendada | 
| --- | --- | --- | 
| TOKEN\$1MALFORMED | 1 | Cree un token válido y vuelva a intentar crear instancias para la fase. | 
| TOKEN\$1EXPIRED | 2 | Cree un token no vencido y vuelva a intentar crear instancias para la fase. | 
| TIMEOUT | 3 | La operación ha agotado el tiempo de espera. Si la fase existe y el token es válido, es probable que este error se deba a un problema de red. En ese caso, espere a que se recupere la conectividad del dispositivo. | 
| ERROR | 4 | Se produjo una condición grave al intentar completar una operación. Compruebe los detalles del error. Si la fase existe y el token es válido, es probable que este error se deba a un problema de red. En ese caso, espere a que se recupere la conectividad del dispositivo. Para la mayoría de las fallas relacionadas con la estabilidad de la red, el SDK reintentará internamente durante un período de hasta 30 segundos antes de emitir un error de tipo FAILED.  | 
| CANCELADO | 5 | Compruebe el código de la aplicación y asegúrese de que no haya invocaciones de `join`, `refreshStrategy` o `replaceStrategy` repetidas, ya que puede provocar que las operaciones repetidas se inicien y cancelen antes de completarse. | 
| STAGE\$1AT\$1CAPACITY | 6 | Este error indica que la etapa o su cuenta ha alcanzado la capacidad máxima. Si la etapa ha alcanzado su límite de participantes, intente la operación nuevamente cuando haya disponibilidad y actualice la estrategia de reintento si es necesario. Si su cuenta ha alcanzado su cuota de suscripciones simultáneas o de publicadores simultáneos, reduzca el uso o solicite un aumento de cuota a través de la [consola de AWS Service Quotas](https://console.aws.amazon.com/servicequotas/).  | 
| CODEC\$1MISMATCH | 7 | La fase no admite el códec. Compruebe si el navegador y la plataforma son compatibles con el códec. Para la transmisión en tiempo real de IVS, los navegadores deben admitir el códec H.264 para video y el códec Opus para audio. | 
| TOKEN\$1NOT\$1ALLOWED | 8 | El token no tiene permiso para la operación. Vuelva a crear el token con los permisos correctos e inténtelo de nuevo. | 
| STAGE\$1DELETED | 9 | Ninguno. Este error se produce al intentar unirse a una etapa que ha sido eliminada. | 
| PARTICIPANT\$1DISCONNECTED | 10 | Ninguno. Este error se produce al intentar unirse con el token de un participante que se ha desconectado. | 

### Ejemplo de gestión de StageError
<a name="web-error-handling-stage-errors-example"></a>

Use el código StageError para determinar si el error se debe a un token vencido:

```
stage.on(StageEvents.ERROR, (error: StageError) => {
    if (error.code === StageError.TOKEN_EXPIRED) {
        // recreate the token and stage instance and re-join
    }
});
```

### Errores de red cuando ya está conectado
<a name="web-error-handling-stage-errors-network"></a>

Si se interrumpe la conexión de red del dispositivo, es posible que el SDK pierda la conexión con los servidores de fases. Es posible que observe errores en la consola porque el SDK ya no puede acceder a los servicios de backend. Las publicaciones en https://broadcast.stats.live-video.net fallarán.

Si está publicando o suscribiéndose, observará errores en la consola relacionados con los intentos de publicar/suscribirte.

Internamente, el SDK intentará reconectarse con una estrategia de retroceso exponencial.

**Acción**: espere a que se recupere la conectividad del dispositivo.

## Estados de error
<a name="web-error-handling-errored-states"></a>

Le recomendamos que utilice estos estados para el registro de las aplicaciones y para mostrar mensajes a los usuarios que les avisen de problemas de conectividad con la fase para un participante concreto.

### Publicación
<a name="errored-states-publish"></a>

El SDK informa `ERRORED` cuando se produce un error en una publicación.

```
stage.on(StageEvents.STAGE_PARTICIPANT_PUBLISH_STATE_CHANGED, (participantInfo, state) => {
  if (state === StageParticipantPublishState.ERRORED) {
      // Log and/or display message to user
  }
});
```

### Suscribirse
<a name="errored-states-subscribe"></a>

El SDK informa `ERRORED` cuando se produce un error en la suscripción. Esto puede ocurrir debido a las condiciones de la red o si una etapa está llena para los suscriptores.

```
stage.on(StageEvents.STAGE_PARTICIPANT_SUBSCRIBE_STATE_CHANGED, (participantInfo, state) => {
  if (state === StageParticipantSubscribeState.ERRORED) {
    // Log and/or display message to user
  }
});
```

# SDK de transmisión de IVS: guía para Android \$1 Transmisión en tiempo real
<a name="broadcast-android"></a>

El SDK de transmisión para transmisión en tiempo real de IVS para Android permite a los participantes enviar y recibir videos en Android.

El paquete de `com.amazonaws.ivs.broadcast` implementa la interfaz descrita en este documento. El SDK admite las siguientes operaciones:
+ Incorporación a un escenario 
+ Publicación de contenido multimedia para otros participantes del escenario
+ Suscripción al contenido multimedia de otros participantes del escenario
+ Administración y monitoreo del video y audio publicados en el escenario
+ Obtención de estadísticas de WebRTC de cada conexión de pares
+ Todas las operaciones del SDK de transmisión para Android de transmisión de baja latencia de IVS

**Última versión del SDK de transmisión para Android:** 1.40.0 ([Notas de la versión](https://docs.aws.amazon.com/ivs/latest/RealTimeUserGuide/release-notes.html#mar12-26-broadcast-android-rt)) 

**Documentación de referencia:** a fin de obtener información sobre los métodos más importantes disponibles en el SDK de transmisión de Android de Amazon IVS, consulte la documentación de referencia en [https://aws.github.io/amazon-ivs-broadcast-docs/1.40.0/android/](https://aws.github.io/amazon-ivs-broadcast-docs/1.40.0/android/).

**Código de muestra: **Consulte el repositorio de muestra de Android en GitHub: [https://github.com/aws-samples/amazon-ivs-real-time-streaming-android-samples](https://github.com/aws-samples/amazon-ivs-real-time-streaming-android-samples).

**Requisitos de la plataforma:** Android 9.0\$1

# Introducción al SDK de transmisión para Android de IVS \$1 Transmisión en tiempo real
<a name="broadcast-android-getting-started"></a>

Este documento explica los pasos necesarios para comenzar a utilizar el SDK de transmisión para Android de la transmisión en tiempo real de IVS.

## Instalación de la biblioteca
<a name="broadcast-android-install"></a>

Hay varias formas de agregar la biblioteca de transmisión para Android de Amazon IVS a su entorno de desarrollo de Android: uso directo de Gradle, uso de los catálogos de versiones de Gradle o instalación manual del SDK.

**Uso directo de Gradle**: 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 IVS):

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

**Uso de los catálogos de versiones de Gradle**: primero incluya esto en el archivo `build.gradle` del módulo:

```
implementation(libs.ivs){
   artifact {
      classifier = "stages"
      type = "aar"
   }
}
```

A continuación, incluya lo siguiente en el archivo `libs.version.toml` (para obtener la versión más reciente del SDK de transmisión de IVS):

```
[versions]
ivs="1.40.0"

[libraries]
ivs = {module = "com.amazonaws:ivs-broadcast", version.ref = "ivs"}
```

**Instalación manual del SDK**: descargue la versión más reciente desde esta ubicación:

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

Asegúrese de descargar el archivo `aar` con `-stages` adjunto.

**Permita también el control de SDK a través del altavoz**: independientemente del método de instalación que elija, agregue también el siguiente permiso al manifiesto para permitir que el SDK habilite o deshabilite el altavoz:

```
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
```

## Uso del SDK con símbolos de depuración
<a name="broadcast-android-using-debug-symbols-rt"></a>

También publicamos una versión del SDK de transmisión para Android que incluye símbolos de depuración. Puede usar esta versión para mejorar la calidad de los informes de depuración (seguimientos de pila) en Firebase Crashlytics si se produce algún fallo en el SDK de transmisión de IVS, es decir, `libbroadcastcore.so`. Cuando notifica estos bloqueos al equipo del SDK de IVS, los rastreos de pila de mayor calidad facilitan la solución de los problemas.

Para usar esta versión del SDK, coloque lo siguiente en los archivos de compilación de Gradle:

```
implementation "com.amazonaws:ivs-broadcast:$version:stages-unstripped@aar"
```

Use la línea anterior en lugar de esta:

```
implementation "com.amazonaws:ivs-broadcast:$version:stages@aar"
```

### Carga de símbolos en Firebase Crashlytics
<a name="android-debug-symbols-rt-firebase-crashlytics"></a>

Asegúrese de que los archivos de compilación de Gradle estén configurados para Firebase Crashlytics. Siga las instrucciones de Google aquí:

[https://firebase.google.com/docs/crashlytics/ndk-reports](https://firebase.google.com/docs/crashlytics/ndk-reports)

Asegúrese de incluir `com.google.firebase:firebase-crashlytics-ndk` como dependencia.

Al crear la aplicación para su lanzamiento, el complemento de Firebase Crashlytics debería cargar los símbolos automáticamente. Para cargar los símbolos manualmente, ejecute cualquiera de los comandos siguientes:

```
gradle uploadCrashlyticsSymbolFileRelease
```

```
./gradlew uploadCrashlyticsSymbolFileRelease
```

(No pasará nada si los símbolos se cargan dos veces, automática y manualmente).

### Cómo evitar que el archivo .apk de la versión aumente de tamaño
<a name="android-debug-symbols-rt-sizing-apk"></a>

Antes de empaquetar el archivo `.apk` de la versión, el complemento de Gradle para Android intenta eliminar automáticamente la información de depuración de las bibliotecas compartidas (incluida la biblioteca `libbroadcastcore.so` del SDK de transmisión de IVS). Sin embargo, a veces esto no sucede. Como resultado, el archivo `.apk` podría aumentar de tamaño y podría recibir un mensaje de advertencia del complemento de Gradle para Android indicándole que no puede eliminar los símbolos de depuración y que está empaquetando los archivos `.so` tal como están. Si esto sucede, haga lo siguiente:
+ Instale un NDK de Android. Cualquier versión reciente funcionará.
+ Agregue `ndkVersion <your_installed_ndk_version_number>` al archivo `build.gradle` de la aplicación. Haga esto incluso si la propia aplicación no contiene código nativo.

Para obtener más información, consulte este [informe de problemas](https://issuetracker.google.com/issues/353554169).

## Solicitar permisos
<a name="broadcast-android-permissions"></a>

La aplicación debe solicitar permiso para acceder a la cámara y al micrófono del usuario. (Esto no es específico de Amazon IVS; es necesario para cualquier aplicación que necesite acceso a cámaras y micrófonos).

Aquí, verificamos si el usuario ya ha concedido permisos y, de no ser así, preguntamos por ellos:

```
final String[] requiredPermissions =
         { Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO };

for (String permission : requiredPermissions) {
    if (ContextCompat.checkSelfPermission(this, permission) 
                != PackageManager.PERMISSION_GRANTED) {
        // If any permissions are missing we want to just request them all.
        ActivityCompat.requestPermissions(this, requiredPermissions, 0x100);
        break;
    }
}
```

Aquí, obtenemos la respuesta del usuario:

```
@Override
public void onRequestPermissionsResult(int requestCode, 
                                      @NonNull String[] permissions,
                                      @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode,
               permissions, grantResults);
    if (requestCode == 0x100) {
        for (int result : grantResults) {
            if (result == PackageManager.PERMISSION_DENIED) {
                return;
            }
        }
        setupBroadcastSession();
    }
}
```

# Publicación y suscripción con el SDK de transmisión para Android de IVS \$1 Transmisión en tiempo real
<a name="android-publish-subscribe"></a>

Este documento explica los pasos necesarios para publicar y suscribirse a una fase mediante el SDK de transmisión para Android de la transmisión en tiempo real de IVS.

## Conceptos
<a name="android-publish-subscribe-concepts"></a>

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

### Etapa
<a name="android-publish-subscribe-concepts-stage"></a>

La clase `Stage` es el principal punto de interacción entre la aplicación host y el SDK. Representa el escenario como tal y se usa para entrar y salir de él. Para crear un escenario e incorporarse a él, es necesaria una cadena de símbolos válida y que no haya vencido del plano de control (representada como `token`). Entrar a un escenario y salir de él es sencillo. 

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

try {
	stage.join();
} catch (BroadcastException exception) {
	// handle join exception
}

stage.leave();
```

También se puede adjuntar `StageRenderer` en la clase `Stage`:

```
stage.addRenderer(renderer); // multiple renderers can be added
```

### Strategy (Estrategia)
<a name="android-publish-subscribe-concepts-strategy"></a>

La interfaz `Stage.Strategy` proporciona una forma para que la aplicación host comunique el estado deseado del escenario al SDK. Es necesario implementar las siguientes tres funciones: `shouldSubscribeToParticipant`, `shouldPublishFromParticipant` y `stageStreamsToPublishForParticipant`. Todas se analizan a continuación.

#### Suscripción a participantes
<a name="android-publish-subscribe-concepts-strategy-participants"></a>

```
Stage.SubscribeType shouldSubscribeToParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo);
```

Cuando un participante remoto se incorpora al escenario, el SDK consulta la aplicación host sobre el estado de suscripción deseado de ese participante. Las opciones son `NONE`, `AUDIO_ONLY` y `AUDIO_VIDEO`. Al devolver un valor para esta función, la aplicación host no tiene que preocuparse por el estado de la publicación, el estado actual de la suscripción ni el estado de la conexión del escenario. Si se devuelve `AUDIO_VIDEO`, el SDK espera a que el participante remoto publique antes de suscribirse y actualiza la aplicación host a través del renderizador durante todo el proceso.

Este es un ejemplo de implementación:

```
@Override
Stage.SubscribeType shouldSubscribeToParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	return Stage.SubscribeType.AUDIO_VIDEO;
}
```

Esta es la implementación completa de esta función para una aplicación host que siempre quiere que todos los participantes se vean entre sí; por ejemplo, una aplicación de videochat.

También son posibles implementaciones más avanzadas. Utilice la propiedad `userInfo` en `ParticipantInfo` para suscribirse de forma selectiva a los participantes en función de los atributos proporcionados por el servidor:

```
@Override
Stage.SubscribeType shouldSubscribeToParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	switch(participantInfo.userInfo.get(“role”)) {
		case “moderator”:
			return Stage.SubscribeType.NONE;
		case “guest”:
			return Stage.SubscribeType.AUDIO_VIDEO;
		default:
			return Stage.SubscribeType.NONE;
	}
}
```

Esto se puede utilizar para crear un escenario en el que los moderadores puedan monitorear a todos los invitados sin que ellos mismos los vean ni los escuchen. La aplicación host podría utilizar una lógica empresarial adicional para permitir que los moderados se vean entre sí, pero permanezcan invisibles para los invitados.

#### Configuración de la suscripción a participantes
<a name="android-publish-subscribe-concepts-strategy-participants-config"></a>

```
SubscribeConfiguration subscribeConfigurationForParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo);
```

Si se está subscribiendo a un participante remoto (consulte [Suscripción a participantes](#android-publish-subscribe-concepts-strategy-participants)), el SDK consulta a la aplicación host sobre una configuración de la subscrición personalizada para ese participante. Esta configuración es opcional y permite a la aplicación host controlar determinados aspectos del comportamiento de los suscriptores. Para obtener información sobre lo que se puede configurar, consulte [SubscribeConfiguration](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/interfaces/SubscribeConfiguration) en la documentación de referencia del SDK.

Este es un ejemplo de implementación:

```
@Override
public SubscribeConfiguration subscribeConfigrationForParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
    SubscribeConfiguration config = new SubscribeConfiguration();

    config.jitterBuffer.setMinDelay(JitterBufferConfiguration.JitterBufferDelay.MEDIUM());

    return config;
}
```

Esta implementación actualiza el retraso mínimo del búfer de fluctuación para todos los participantes suscritos al valor preestablecido `MEDIUM`.

Como en el caso de `shouldSubscribeToParticipant`, también son posibles implementaciones más avanzadas. El valor de `ParticipantInfo` proporcionado se puede utilizar para actualizar selectivamente la configuración de suscripción para participantes específicos.

Se recomienda utilizar los comportamientos predeterminados. Especifique la configuración personalizada solo si hay un comportamiento en particular que desee cambiar.

#### Publicación
<a name="android-publish-subscribe-concepts-strategy-publishing"></a>

```
boolean shouldPublishFromParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo);
```

Una vez realizada la conexión al escenario, el SDK consulta la aplicación host para ver si un participante en particular tiene que publicar. Esto solo se invoca en los participantes locales que tienen permiso para publicar en función del token proporcionado.

Este es un ejemplo de implementación:

```
@Override
boolean shouldPublishFromParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	return true;
}
```

Esto es para una aplicación de videochat estándar en la que los usuarios siempre quieren publicar. Pueden activar y desactivar el sonido y el video para ocultarse o verse y escucharse al instante. (También pueden usar la opción de publicar o anular la publicación, pero esto es mucho más lento. Es preferible silenciar o activar el sonido en casos de uso en los que se quiera cambiar la visibilidad de manera frecuente).

#### Elección de las transmisiones que publicar
<a name="android-publish-subscribe-concepts-strategy-streams"></a>

```
@Override
List<LocalStageStream> stageStreamsToPublishForParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo);
}
```

Al publicar, esto se utiliza para determinar qué transmisiones de audio y video se tienen que publicar. Esto se explica en mayor detalle más adelante en [Publicación de una transmisión multimedia](#android-publish-subscribe-publish-stream).

#### Actualización de la estrategia
<a name="android-publish-subscribe-concepts-strategy-updates"></a>

La estrategia pretende ser dinámica: los valores devueltos por cualquiera de las funciones anteriores se pueden cambiar en cualquier momento. Por ejemplo, si la aplicación host no quiere publicar hasta que el usuario final presione un botón, puede devolver una variable de `shouldPublishFromParticipant` (como `hasUserTappedPublishButton`). Cuando esa variable cambie en función de una interacción del usuario final, llame a `stage.refreshStrategy()` para indicar al SDK que debe consultar la estrategia a fin de obtener los valores más recientes y aplicar solo los cambios. Si el SDK observa que el valor `shouldPublishFromParticipant` cambió, iniciará el proceso de publicación. Si las consultas del SDK y todas las funciones devuelven el mismo valor que antes, la llamada a `refreshStrategy` no hará ninguna modificación en el escenario.

Si el valor devuelto de `shouldSubscribeToParticipant` cambia de `AUDIO_VIDEO` a `AUDIO_ONLY`, la transmisión de video se eliminará para todos los participantes con valores devueltos modificados, si ya existía con anterioridad una transmisión de video.

Por lo general, el escenario utiliza la estrategia para aplicar de la manera más eficiente la diferencia entre las estrategias anteriores y actuales, sin que la aplicación host tenga que preocuparse por todo el estado necesario para administrarla correctamente. Por eso, piense en la llamada a `stage.refreshStrategy()` como una operación barata, porque no hace nada a no ser que cambie la estrategia.

### Renderer
<a name="android-publish-subscribe-concepts-renderer"></a>

La interfaz `StageRenderer` comunica el estado del escenario a la aplicación host. Por lo general, los eventos proporcionados por el renderizador permiten el funcionamiento completo de las actualizaciones de la interfaz de usuario de la aplicación host. El renderizador ofrece el siguiente resultado:

```
void onParticipantJoined(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo);

void onParticipantLeft(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo);

void onParticipantPublishStateChanged(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull Stage.PublishState publishState);

void onParticipantSubscribeStateChanged(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull Stage.SubscribeState subscribeState);

void onStreamsAdded(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull List<StageStream> streams);

void onStreamsRemoved(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull List<StageStream> streams);

void onStreamsMutedChanged(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull List<StageStream> streams);

void onError(@NonNull BroadcastException exception);

void onConnectionStateChanged(@NonNull Stage stage, @NonNull Stage.ConnectionState state, @Nullable BroadcastException exception);
                
void onStreamAdaptionChanged(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull RemoteStageStream stream, boolean adaption);

void onStreamLayersChanged(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull RemoteStageStream stream, @NonNull List<RemoteStageStream.Layer> layers);

void onStreamLayerSelected(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull RemoteStageStream stream, @Nullable RemoteStageStream.Layer layer, @NonNull RemoteStageStream.LayerSelectedReason reason);
```

Para la mayoría de estos métodos, se proporcionan el `Stage` y la `ParticipantInfo` correspondientes.

No se espera que la información que proporciona el renderizador afecte a los valores de retorno de la estrategia. Por ejemplo, no se espera que el valor devuelto de `shouldSubscribeToParticipant` cambie cuando se llama a `onParticipantPublishStateChanged`. Si la aplicación host quiere suscribirse a un participante en particular, debe devolver el tipo de suscripción deseado, independientemente del estado de publicación de ese participante. El SDK es responsable de garantizar que se aplique el estado deseado de la estrategia en el momento correcto según el estado del escenario.

`StageRenderer` se puede adjuntar a la clase del escenario:

```
stage.addRenderer(renderer); // multiple renderers can be added
```

Tenga en cuenta que solo los participantes que publican desencadenan `onParticipantJoined`. `onParticipantLeft` se desencadena cada vez que un participante deja de publicar o abandona la sesión del escenario.

## Publicación de una transmisión multimedia
<a name="android-publish-subscribe-publish-stream"></a>

Los dispositivos locales, como las cámaras y los micrófonos integrados, se detectan a través de `DeviceDiscovery`. A continuación, se muestra un ejemplo de cómo seleccionar la cámara frontal y el micrófono predeterminados y devolverlos como `LocalStageStreams` para que los publique el SDK:

```
DeviceDiscovery deviceDiscovery = new DeviceDiscovery(context);

List<Device> devices = deviceDiscovery.listLocalDevices();
List<LocalStageStream> publishStreams = new ArrayList<LocalStageStream>();

Device frontCamera = null;
Device microphone = null;

// Create streams using the front camera, first microphone
for (Device device : devices) {
	Device.Descriptor descriptor = device.getDescriptor();
	if (!frontCamera && descriptor.type == Device.Descriptor.DeviceType.Camera && descriptor.position = Device.Descriptor.Position.FRONT) {
		front Camera = device;
	}
	if (!microphone && descriptor.type == Device.Descriptor.DeviceType.Microphone) {
		microphone = device;
	}
}

ImageLocalStageStream cameraStream = new ImageLocalStageStream(frontCamera);
AudioLocalStageStream microphoneStream = new AudioLocalStageStream(microphoneDevice);

publishStreams.add(cameraStream);
publishStreams.add(microphoneStream);

// Provide the streams in Stage.Strategy
@Override
@NonNull List<LocalStageStream> stageStreamsToPublishForParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	return publishStreams;
}
```

## Visualización y eliminación de participantes
<a name="android-publish-subscribe-participants"></a>

Cuando se complete la suscripción, recibirá una matriz de objetos `StageStream` a través de la función `onStreamsAdded` del renderizador. Puede obtener la vista previa de una `ImageStageStream`:

```
ImagePreviewView preview = ((ImageStageStream)stream).getPreview();

// Add the view to your view hierarchy
LinearLayout previewHolder = findViewById(R.id.previewHolder);
preview.setLayoutParams(new LinearLayout.LayoutParams(
		LinearLayout.LayoutParams.MATCH_PARENT,
		LinearLayout.LayoutParams.MATCH_PARENT));
previewHolder.addView(preview);
```

Puede recuperar las estadísticas de audio de una `AudioStageStream`:

```
((AudioStageStream)stream).setStatsCallback((peak, rms) -> {
	// handle statistics
});
```

Cuando un participante deja de publicar o anula su suscripción, se invoca la función `onStreamsRemoved` con las transmisiones que se eliminaron. Las aplicaciones host tienen que usar esto como una señal para eliminar la transmisión de video del participante de la jerarquía de visualización.

`onStreamsRemoved` se invoca para todas las situaciones en las que se puede eliminar una transmisión, como las siguientes: 
+ El participante remoto deja de publicar.
+ Un dispositivo local cancela la suscripción o cambia la suscripción de `AUDIO_VIDEO` a `AUDIO_ONLY`.
+ El participante remoto abandona el escenario.
+ El participante local abandona el escenario.

Ya que `onStreamsRemoved` se invoca en todas las situaciones, no es necesaria una lógica empresarial personalizada para eliminar a los participantes de la interfaz de usuario durante las operaciones de licencia remota o local.

## Activación y desactivación del sonido de las transmisiones multimedia
<a name="android-publish-subscribe-mute-streams"></a>

Los objetos de `LocalStageStream` tienen una función `setMuted` que controla si la transmisión está silenciada. Esta función se puede invocar en la transmisión antes o después de que la devuelva la función de estrategia `streamsToPublishForParticipant`.

**Importante**: Si `streamsToPublishForParticipant` devuelve una nueva instancia de objeto `LocalStageStream` después de una llamada a `refreshStrategy`, el estado de sonido desactivado del nuevo objeto de transmisión se aplicará al escenario. Tenga cuidado al crear nuevas instancias `LocalStageStream` para asegurarse de que se mantenga el estado de sonido desactivado que se espera.

## Monitoreo del estado de sonido desactivado en los contenidos multimedia del participante remoto
<a name="android-publish-subscribe-mute-state"></a>

Cuando un participante cambia el estado de sonido desactivado de su transmisión de video o audio, se invoca la función `onStreamMutedChanged` de renderizado con una lista de las transmisiones que cambiaron. Use el método `getMuted` en `StageStream` para actualizar la interfaz de usuario según corresponda. 

```
@Override
void onStreamsMutedChanged(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull List<StageStream> streams) {
	for (StageStream stream : streams) {
		boolean muted = stream.getMuted();
		// handle UI changes
	}
}
```

## Obtención de estadísticas de WebRTC
<a name="android-publish-subscribe-webrtc-stats"></a>

Para obtener las estadísticas más recientes de WebRTC de una transmisión de publicación o de suscripción, utilice `requestRTCStats` en `StageStream`. Cuando se complete una recopilación, recibirá estadísticas a través de `StageStream.Listener`, que se puede configurar en `StageStream`.

```
stream.requestRTCStats();

@Override
void onRTCStats(Map<String, Map<String, String>> statsMap) {
	for (Map.Entry<String, Map<String, string>> stat : statsMap.entrySet()) {
		for(Map.Entry<String, String> member : stat.getValue().entrySet()) {
			Log.i(TAG, stat.getKey() + “ has member “ + member.getKey() + “ with value “ + member.getValue());
		}
	}
}
```

## Obtención de los atributos de los participantes
<a name="android-publish-subscribe-participant-attributes"></a>

Si especifica atributos en la solicitud de la operación de `CreateParticipantToken`, podrá ver los atributos en las propiedades de `ParticipantInfo`:

```
@Override
void onParticipantJoined(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	for (Map.Entry<String, String> entry : participantInfo.userInfo.entrySet()) {
		Log.i(TAG, “attribute: “ + entry.getKey() + “ = “ + entry.getValue());
	}
}
```

## Mensajes incrustados
<a name="android-publish-subscribe-embed-messages"></a>

El método `embedMessage` en ImageDevice permite insertar cargas útiles de metadatos directamente en los fotogramas de video durante la publicación. Esto activa la mensajería sincronizada por fotogramas para aplicaciones en tiempo real. La incrustación de mensajes solo está disponible cuando se utiliza el SDK para la publicación en tiempo real (no para la publicación de baja latencia).

No se garantiza que los mensajes incrustados lleguen a los suscriptores porque se incrustan directamente en los fotogramas de video y se transmiten a través de UDP, lo que no asegura la entrega de paquetes. La pérdida de paquetes durante la transmisión puede provocar la pérdida de mensajes, especialmente en condiciones de red deficientes. Para mitigar esta situación, el método `embedMessage` incluye un parámetro `repeatCount` que duplica el mensaje en varios fotogramas consecutivos, lo que aumenta la fiabilidad de la entrega. Esta capacidad solo está disponible para transmisiones de video.

### Uso de embedMessage
<a name="android-embed-messages-using-embedmessage"></a>

Los clientes de publicación pueden incrustar cargas útiles de mensajes en su transmisión de video mediante el método `embedMessage` de ImageDevice. El tamaño de la carga útil debe ser superior a 0 KB e inferior a 1 KB. El número de mensajes incrustados insertados por segundo no debe superar los 10 KB por segundo. 

```
val surfaceSource: SurfaceSource = imageStream.device as SurfaceSource
val message = "hello world"
val messageBytes = message.toByteArray(StandardCharsets.UTF_8)

try {
    surfaceSource.embedMessage(messageBytes, 0)
} catch (e: BroadcastException) {
    Log.e("EmbedMessage", "Failed to embed message: ${e.message}")
}
```

### Cargas útiles de mensaje que se repiten
<a name="android-embed-messages-repeat-payloads"></a>

Utilice `repeatCount` para duplicar el mensaje en varios marcos a fin de mejorar la fiabilidad. Este valor debe estar entre 0 y 30. Los clientes receptores deben tener una lógica para desduplicar el mensaje.

```
try {
    surfaceSource.embedMessage(messageBytes, 5)
    // repeatCount: 0-30, receiving clients should handle duplicates
} catch (e: BroadcastException) {
    Log.e("EmbedMessage", "Failed to embed message: ${e.message}")
}
```

### Lectura de mensajes incrustados
<a name="android-embed-messages-read-messages"></a>

Consulte la sección “Obtención de información de mejora adicional (SEI)” a continuación para saber cómo leer los mensajes incrustados de las transmisiones entrantes.

## Obtención de información de mejora adicional (SEI)
<a name="android-publish-subscribe-sei-attributes"></a>

La unidad NAL de información de mejora adicional (SEI) se usa para almacenar metadatos alineados con los fotogramas junto con el video. Los clientes suscriptores pueden leer las cargas útiles SEI de un publicador que publica un video H.264 mediante la inspección de la propiedad `embeddedMessages` en los objetos `ImageDeviceFrame` que salen del `ImageDevice` del publicador. Para ello, adquiera el `ImageDevice` de un publicador y, a continuación, observe cada fotograma a través de una devolución de llamada proporcionada a `setOnFrameCallback`, como se muestra en el siguiente ejemplo:

```
// in a StageRenderer’s onStreamsAdded function, after acquiring the new ImageStream

val imageDevice = imageStream.device as ImageDevice
imageDevice.setOnFrameCallback(object : ImageDevice.FrameCallback {
	override fun onFrame(frame: ImageDeviceFrame) {
    		for (message in frame.embeddedMessages) {
        		if (message is UserDataUnregisteredSeiMessage) {
            		val seiMessageBytes = message.data
            		val seiMessageUUID = message.uuid
           	 
            		// interpret the message's data based on the UUID
        		}
    		}
	}
})
```

## Continuación de la sesión en segundo plano
<a name="android-publish-subscribe-background-session"></a>

Cuando la aplicación pase a segundo plano, es posible que quiera dejar de publicar o suscribirse solo al audio de otros participantes remotos. Para ello, actualice la implementación `Strategy` para detener la publicación y suscríbase a `AUDIO_ONLY` (o a `NONE`, si corresponde).

```
// Local variables before going into the background
boolean shouldPublish = true;
Stage.SubscribeType subscribeType = Stage.SubscribeType.AUDIO_VIDEO;

// Stage.Strategy implementation
@Override
boolean shouldPublishFromParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	return shouldPublish;
}

@Override
Stage.SubscribeType shouldSubscribeToParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	return subscribeType;
}

// In our Activity, modify desired publish/subscribe when we go to background, then call refreshStrategy to update the stage
@Override
void onStop() {
	super.onStop();
	shouldPublish = false;
	subscribeTpye = Stage.SubscribeType.AUDIO_ONLY;
	stage.refreshStrategy();
}
```

## Codificación por capas con la transmisión simultánea
<a name="android-publish-subscribe-layered-encoding-simulcast"></a>

La codificación por capas con transmisión simultánea es una característica de transmisión en tiempo real de IVS que permite a quienes publican enviar múltiples capas de video con diferentes niveles de calidad, mientras que los suscriptores pueden configurar estas capas de manera dinámica o manual. Esta característica se describe con más detalle en el documento [Optimizaciones de transmisión](real-time-streaming-optimization.md).

### Configuración de la codificación por capas (publicador)
<a name="android-layered-encoding-simulcast-configure-publisher"></a>

Como publicador, para habilitar la codificación por capas con transmisión simultánea, agregue la siguiente configuración a `LocalStageStream` en la instanciación:

```
// Enable Simulcast
StageVideoConfiguration config = new StageVideoConfiguration();
config.simulcast.setEnabled(true);

ImageLocalStageStream cameraStream = new ImageLocalStageStream(frontCamera, config);

// Other Stage implementation code
```

En función de la resolución establecida en la configuración de video, se codificará y enviará una cantidad determinada de capas, tal como se define en la sección [Capas, calidades y velocidades de fotogramas predeterminadas](real-time-streaming-optimization.md#real-time-streaming-optimization-default-layers) de *Optimizaciones de transmisión*.

Además, si lo desea, puede configurar capas individuales desde la configuración de transmisión simultánea: 

```
// Enable Simulcast
StageVideoConfiguration config = new StageVideoConfiguration();
config.simulcast.setEnabled(true);

List<StageVideoConfiguration.Simulcast.Layer> simulcastLayers = new ArrayList<>();
simulcastLayers.add(StagePresets.SimulcastLocalLayer.DEFAULT_720);
simulcastLayers.add(StagePresets.SimulcastLocalLayer.DEFAULT_180);

config.simulcast.setLayers(simulcastLayers);

ImageLocalStageStream cameraStream = new ImageLocalStageStream(frontCamera, config);

// Other Stage implementation code
```

De manera opcional, también puede crear sus propias configuraciones de capas personalizadas para hasta tres capas. Si proporciona una matriz vacía o ningún valor, se utilizan los valores predeterminados descritos con anterioridad. Las capas se describen con los siguientes setters de propiedades obligatorios:
+ `setSize: Vec2;`
+ `setMaxBitrate: integer;`
+ `setMinBitrate: integer;`
+ `setTargetFramerate: integer;`

A partir de los ajustes preestablecidos, puede anular las propiedades individuales o crear una configuración completamente nueva:

```
// Enable Simulcast
StageVideoConfiguration config = new StageVideoConfiguration();
config.simulcast.setEnabled(true);

List<StageVideoConfiguration.Simulcast.Layer> simulcastLayers = new ArrayList<>();

// Configure high quality layer with custom framerate
StageVideoConfiguration.Simulcast.Layer customHiLayer = StagePresets.SimulcastLocalLayer.DEFAULT_720;
customHiLayer.setTargetFramerate(15);

// Add layers to the list
simulcastLayers.add(customHiLayer);
simulcastLayers.add(StagePresets.SimulcastLocalLayer.DEFAULT_180);

config.simulcast.setLayers(simulcastLayers);

ImageLocalStageStream cameraStream = new ImageLocalStageStream(frontCamera, config);

// Other Stage implementation code
```

Para conocer los valores máximos, los límites y los errores que se pueden activar al configurar capas individuales, consulta la documentación de referencia del SDK.

### Configuración de la codificación por capas (suscriptor)
<a name="android-layered-encoding-simulcast-configure-subscriber"></a>

Como suscriptor, no es necesario hacer nada para habilitar la codificación por capas. Si un publicador envía capas de transmisión simultánea, el servidor, de forma predeterminada, se adaptará dinámicamente entre las capas para elegir la calidad óptima en función de las condiciones de la red y del dispositivo del suscriptor.

Como alternativa, existen varias opciones para seleccionar las capas explícitas que envía el publicador, tal y como se describe a continuación.

### Opción 1: preferencia de calidad de la capa inicial
<a name="android-layered-encoding-simulcast-layer-quality-preference"></a>

Mediante la estrategia `subscribeConfigurationForParticipant`, es posible elegir qué capa inicial desea recibir como suscriptor:

```
@Override
public SubscribeConfiguration subscribeConfigrationForParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
    SubscribeConfiguration config = new SubscribeConfiguration();

    config.simulcast.setInitialLayerPreference(SubscribeSimulcastConfiguration.InitialLayerPreference.LOWEST_QUALITY);

    return config;
}
```

De forma predeterminada, a los suscriptores siempre se les envía primero la capa de calidad más baja; poco a poco se incrementa hasta llegar a la capa de calidad más alta. Esto optimiza el consumo de ancho de banda del usuario final y proporciona el mejor tiempo de inicio del video, lo cual reduce las congelaciones iniciales de video para los usuarios en redes más débiles.

Estas opciones se encuentran disponibles para `InitialLayerPreference`:
+ `LOWEST_QUALITY`: el servidor entrega primero la capa de video de menor calidad. Esto optimiza tanto el consumo de ancho de banda como el tiempo de inicio del contenido multimedia. La calidad se define como la combinación de tamaño, velocidad de bits y velocidad de fotogramas del video. Por ejemplo, un video 720p es de menor calidad que un video 1080p.
+ `HIGHEST_QUALITY`: el servidor entrega primero la capa de video de mayor calidad. Esto optimiza la calidad, pero puede aumentar el tiempo de inicio del contenido multimedia. La calidad se define como la combinación de tamaño, velocidad de bits y velocidad de fotogramas del video. Por ejemplo, el video 1080p es de mayor calidad que el video 720p.

**Nota:** Para que se apliquen las preferencias de la capa inicial (la llamada `setInitialLayerPreference`), es necesario volver a suscribirse, ya que estas actualizaciones no se aplican a la suscripción activa.

### Opción 2: capa preferida para la transmisión
<a name="android-layered-encoding-simulcast-preferred-layer"></a>

El método de estrategia `preferredLayerForStream` le permite seleccionar una capa después de que la transmisión haya comenzado. Este método de estrategia recibe la información del participante y de la transmisión, por lo que puede seleccionar una capa para cada participante de forma individual. El SDK llama a este método en respuesta a eventos específicos, como cuando cambian las capas de transmisión o el estado del participante o la aplicación host actualiza la estrategia.

El método de estrategia devuelve un objeto `RemoteStageStream.Layer`, que puede ser uno de los siguientes:
+ Un objeto de capa, como uno devuelto por `RemoteStageStream.getLayers`.
+ nulo, que indica que no se debe seleccionar ninguna capa y que se prefiere la adaptación dinámica.

Por ejemplo, la siguiente estrategia hará que los usuarios seleccionen siempre la capa de video de menor calidad disponible:

```
@Nullable
@Override
public RemoteStageStream.Layer preferredLayerForStream(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull RemoteStageStream stream) {
    return stream.getLowestQualityLayer();
}
```

Para restablecer la selección de capas y volver a la adaptación dinámica, devuelva nulo o sin definir en la estrategia. En este ejemplo, `appState` es una variable de marcador de posición que representa el estado de la aplicación host.

```
@Nullable
@Override
public RemoteStageStream.Layer preferredLayerForStream(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull RemoteStageStream stream) {
    if (appState.isAutoMode) {
        return null;
    } else {
        return appState.layerChoice;
    }
}
```

### Opción 3: ayudantes de la capa RemoteStageStream
<a name="android-layered-encoding-simulcast-remotestagestream-helpers"></a>

`RemoteStageStream` cuenta con varios ayudantes que se pueden utilizar para tomar decisiones sobre la selección de capas y mostrar las selecciones correspondientes a los usuarios finales:
+ **Eventos de capa**: además de `StageRenderer`, `RemoteStageStream.Listener` cuenta con eventos que comunican los cambios de adaptación de capas y transmisión simultánea:
  + `void onAdaptionChanged(boolean adaption)`
  + `void onLayersChanged(@NonNull List<Layer> layers)`
  + `void onLayerSelected(@Nullable Layer layer, @NonNull LayerSelectedReason reason)`
+ **Métodos de capa**: `RemoteStageStream` tiene varios métodos de ayudante que se pueden utilizar para obtener información sobre la transmisión y las capas que se presentan. Estos métodos están disponibles en la transmisión remota proporcionada en la estrategia `preferredLayerForStream`, así como en las transmisiones remotas expuestas a través de `StageRenderer.onStreamsAdded`.
  + `stream.getLayers`
  + `stream.getSelectedLayer`
  + `stream.getLowestQualityLayer`
  + `stream.getHighestQualityLayer`
  + `stream.getLayersWithConstraints`

Para conocer los detalles, consulte la clase `RemoteStageStream` en la [documentación de referencia del SDK](https://aws.github.io/amazon-ivs-broadcast-docs/latest/android/). En cuanto a la razón `LayerSelected`, si se devuelve `UNAVAILABLE`, esto indica que no se ha podido seleccionar la capa solicitada. En su lugar, se selecciona la mejor opción posible, que suele ser una capa de menor calidad para mantener la estabilidad de la transmisión.

## Limitaciones de la configuración de video
<a name="android-publish-subscribe-video-limits"></a>

El SDK no permite forzar el uso del modo vertical ni del horizontal mediante `StageVideoConfiguration.setSize(BroadcastConfiguration.Vec2 size)`. En la orientación vertical, la dimensión más pequeña se utiliza como el ancho; en la orientación horizontal, como la altura. Esto significa que las dos siguientes llamadas a `setSize` tendrán el mismo efecto en la configuración de video:

```
StageVideo Configuration config = new StageVideo Configuration();

config.setSize(BroadcastConfiguration.Vec2(720f, 1280f);
config.setSize(BroadcastConfiguration.Vec2(1280f, 720f);
```

## Gestión de los problemas de red
<a name="android-publish-subscribe-network-issues"></a>

Cuando se pierde la conexión de red del dispositivo local, el SDK se intenta volver a conectar internamente sin que el usuario lleve a cabo ninguna acción. En algunos casos, el SDK no funciona de manera correcta y es necesario que el usuario actúe. Hay dos errores principales relacionados con la pérdida de la conexión de red:
+ Código de error 1400, mensaje: “Se ha perdido la conexión de pares debido a un error de red desconocido”
+ Código de error 1300, mensaje: “Se han agotado los reintentos”

Si se recibe el primer error, pero no el segundo, el SDK sigue conectado al escenario e intentará restablecer sus conexiones automáticamente. Como medida de seguridad, puede hacer una llamada a `refreshStrategy` sin cambiar los valores devueltos por el método de estrategia para iniciar un intento de reconexión manual.

Si se recibe el segundo error, los intentos de reconexión del SDK fallaron y el dispositivo local ya no está conectado al escenario. En este caso, intente reincorporarse al escenario con una llamada a `join` después de que se restablezca la conexión de red.

En general, el que aparezcan errores después de incorporarse a un escenario correctamente indica que el SDK no pudo restablecer la conexión. Cree un nuevo objeto `Stage` e intente incorporarse cuando mejoren las condiciones de la red.

## Uso de micrófonos Bluetooth
<a name="android-publish-subscribe-bluetooth-microphones"></a>

Para publicar con dispositivos de micrófono Bluetooth, debe iniciar una conexión SCO Bluetooth:

```
Bluetooth.startBluetoothSco(context);
// Now bluetooth microphones can be used
…
// Must also stop bluetooth SCO
Bluetooth.stopBluetoothSco(context);
```

# Problemas conocidos y soluciones alternativas del SDK de transmisión para Android de IVS \$1 Transmisión en tiempo real
<a name="broadcast-android-known-issues"></a>

Este documento enumera problemas conocidos que puede experimentar al utilizar el SDK de transmisión para Android de la transmisión en tiempo real de Amazon IVS y sugiere posibles soluciones alternativas.
+ Cuando un dispositivo Android se pone en reposo y se activa, es posible que la vista previa esté congelada.

  **Solución alternativa:** cree y utilice un nuevo `Stage`.
+ Cuando un participante se incorpora con un token que utiliza otro participante, la primera conexión se pierde sin que se produzca un error específico.

  **Solución alternativa:** ninguna. 
+ Hay un problema poco frecuente en el que el editor publica, pero el estado de publicación que reciben los suscriptores es `inactive`.

  **Solución alternativa:** pruebe a salir de la sesión y, a continuación, reincorporarse. Si el problema persiste, cree un nuevo token para el publicador.
+ Durante una sesión del escenario, se puede producir un problema intermitente y poco frecuente de distorsión de audio, por lo general en llamadas de mayor duración.

  **Solución alternativa:** el participante con audio distorsionado puede abandonar la sesión y volver a unirse a ella o anular la publicación y volver a publicar su audio para solucionar el problema.
+ No se admiten micrófonos externos cuando se hacen publicaciones en un escenario.

  **Solución alternativa:** no utilice un micrófono externo conectado por USB para publicar en un escenario.
+ No se admite el uso de `createSystemCaptureSources` para publicar en un escenario con pantalla compartida.

  **Solución alternativa:** administre la captura del sistema de forma manual mediante orígenes de entrada de imágenes personalizados y orígenes de entrada de audio personalizados.
+ Cuando se elimina una `ImagePreviewView` de un elemento principal (por ejemplo, se llama a `removeView()` en el elemento principal), `ImagePreviewView` se libera inmediatamente. La `ImagePreviewView` no muestra ningún fotograma cuando se agrega a otra vista principal.

  **Solución alternativa:** solicite otra vista previa mediante `getPreview`.
+ Al incorporarse a un escenario con un Samsung Galaxy S22/\$1 con Android 12, es posible que aparezca un error 1401 y que el dispositivo local no pueda incorporarse al escenario o lo haga, pero no haya audio.

  **Solución alternativa:** actualice a Android 13.
+ Al incorporarse a un escenario con un Nokia X20 en Android 13, es posible que la cámara no se abra y se produzca una excepción.

  **Solución alternativa:** ninguna.
+ Es posible que los dispositivos con el conjunto de chips MediaTek Helio no reproduzcan correctamente el video de los participantes remotos.

  **Solución alternativa:** ninguna.
+ En algunos dispositivos, el sistema operativo del dispositivo puede elegir un micrófono diferente al seleccionado a través del SDK. Esto se debe a que el SDK de transmisión de Amazon IVS no puede controlar la forma en que se define la ruta de audio de `VOICE_COMMUNICATION`, ya que varía según los distintos fabricantes de dispositivos.

  **Solución alternativa:** ninguna.
+ Algunos codificadores de video de Android no se pueden configurar con un tamaño de video inferior a 176 x 176. La configuración de un tamaño más pequeño provoca un error e impide la transmisión.

  **Solución alternativa:** no configure el tamaño del video para que sea inferior a 176 x 176.

# Control de errores en el SDK de transmisión para Android de IVS \$1 Transmisión en tiempo real
<a name="broadcast-android-error-handling"></a>

En esta sección se ofrece información general sobre las condiciones de error, cómo el SDK de transmisión para Android las notifica a la aplicación y qué debe hacer una aplicación cuando se producen esos errores.

## Errores fatales frente a errores no fatales
<a name="broadcast-android-fatal-vs-nonfatal-errors"></a>

El objeto de error tiene un campo booleano “es grave” de `BroadcastException`.

En general, los errores fatales están relacionados con la conexión al servidor de etapas (o bien no se puede establecer una conexión o bien se pierde y no se puede recuperar). La aplicación debe volver a crear el escenario y volver a unirse, posiblemente con un nuevo token o cuando se recupere la conectividad del dispositivo.

Los errores no fatales suelen estar relacionados con el estado de publicación/suscripción y son gestionados por el SDK, que reintenta la operación de publicación/suscripción.

Puede comprobar esta propiedad:

```
try {
  stage.join(...)
} catch (e: BroadcastException) {
  If (e.isFatal) { 
    // the error is fatal
```

## Errores de unión
<a name="broadcast-android-stage-join-errors"></a>

### Token con formato incorrecto
<a name="broadcast-android-stage-join-errors-malformed-token"></a>

Esto ocurre cuando el token de la etapa tiene un formato incorrecto.

El SDK genera una excepción de Java al llamar a `stage.join`, con el código de error =1000 y fatal =true.

**Acción**: cree un token válido e intente unirse de nuevo.

### Token vencido
<a name="broadcast-android-stage-join-errors-expired-token"></a>

Esto ocurre cuando el token de la etapa está caducado.

El SDK genera una excepción de Java al llamar a `stage.join`, con el código de error =1001 y fatal =true.

**Acción**: cree un token nuevo e intente unirse de nuevo.

### Token no válido o revocado
<a name="broadcast-android-stage-join-errors-invalid-token"></a>

Esto ocurre cuando el token de la etapa no tiene un formato incorrecto, sino que el servidor de etapas lo rechaza. Esto se informa de forma asíncrona a través del renderizador de la etapa suministrado por la aplicación.

El SDK llama a `onConnectionStateChanged` con una excepción, con el código de error =1026 y fatal =true.

**Acción**: cree un token válido e intente unirse de nuevo.

### Errores de red durante la unión inicial
<a name="broadcast-android-stage-join-errors-network-initial-join"></a>

Esto ocurre cuando el SDK no puede ponerse en contacto con el servidor de etapas para establecer una conexión. Esto se informa de forma asíncrona a través del renderizador de la etapa suministrado por la aplicación.

El SDK llama a `onConnectionStateChanged` con una excepción, con el código de error =1300 y fatal =true.

**Acción**: espere a que se recupere la conectividad del dispositivo e intente conectarse de nuevo.

### Errores de red cuando ya está conectado
<a name="broadcast-android-stage-join-errors-network-already-joined"></a>

Si se interrumpe la conexión de red del dispositivo, es posible que el SDK pierda la conexión con los servidores de etapas. Esto se informa de forma asíncrona a través del renderizador de la etapa suministrado por la aplicación.

El SDK llama a `onConnectionStateChanged` con una excepción, con el código de error =1300 y fatal =true.

**Acción**: espere a que se recupere la conectividad del dispositivo e intente conectarse de nuevo.

## Errores de publicación/suscripción
<a name="broadcast-android-publish-subscribe-errors"></a>

### Inicial
<a name="broadcast-android-publish-subscribe-errors-initial"></a>

Existen varios errores:
+ MultihostSessionOfferCreationFailPublish (1020)
+ MultihostSessionOfferCreationFailSubscribe (1021)
+ MultihostSessionNoIceCandidates (1022)
+ MultihostSessionStageAtCapacity (1024)
+ SignallingSessionCannotRead (1201)
+ SignallingSessionCannotSend (1202)
+ SignallingSessionBadResponse (1203)

Estos se informan de forma asíncrona a través del renderizador de la etapa suministrado por la aplicación.

El SDK vuelve a intentar la operación un número limitado de veces. Durante los reintentos, el estado de publicación/suscripción es `ATTEMPTING_PUBLISH`/`ATTEMPTING_SUBSCRIBE`. Si el reintento se realiza correctamente, el estado cambia a `PUBLISHED`/`SUBSCRIBED`.

El SDK llama a `onError` con el código de error correspondiente y fatal =false.

**Acción**: no es necesario realizar ninguna acción, ya que el SDK vuelve a intentarlo automáticamente. Si lo desea, la aplicación puede actualizar la estrategia para forzar más reintentos.

### Ya está establecido, luego falla
<a name="broadcast-android-publish-subscribe-errors-established"></a>

Una publicación o una suscripción pueden fallar una vez establecidas, muy probablemente debido a un error de red. El código de error para “Se ha perdido la conexión de pares debido a un error de red” es 1400.

Esto se informa de forma asíncrona a través del renderizador de la etapa suministrado por la aplicación.

El SDK vuelve a intentar la operación de publicación/suscripción. Durante los reintentos, el estado de publicación/suscripción es `ATTEMPTING_PUBLISH`/`ATTEMPTING_SUBSCRIBE`. Si el reintento se realiza correctamente, el estado cambia a `PUBLISHED`/`SUBSCRIBED`.

El SDK llama a `onError` con el código de error =1400 y fatal =false.

**Acción**: no es necesario realizar ninguna acción, ya que el SDK vuelve a intentarlo automáticamente. Si lo desea, la aplicación puede actualizar la estrategia para forzar más reintentos. En caso de pérdida total de conectividad, es probable que la conexión a las etapas también falle.

# SDK de transmisión de IVS: guía para iOS \$1 Transmisión en tiempo real
<a name="broadcast-ios"></a>

El SDK de transmisión para transmisión en tiempo real de IVS para iOS permite a los participantes enviar y recibir videos en iOS.

El módulo `AmazonIVSBroadcast` implementa la interfaz descrita en este documento. Se admiten las siguientes operaciones:
+ Incorporación a un escenario 
+ Publicación de contenido multimedia para otros participantes del escenario
+ Suscripción al contenido multimedia de otros participantes del escenario
+ Administración y monitoreo del video y audio publicados en el escenario
+ Obtención de estadísticas de WebRTC de cada conexión de pares
+ Todas las operaciones del SDK de transmisión para iOS de transmisión de baja latencia de IVS

**Versión más reciente del SDK de transmisión para iOS:** 1.40.0 ([Notas de la versión](https://docs.aws.amazon.com/ivs/latest/RealTimeUserGuide/release-notes.html#mar12-26-broadcast-ios-rt)) 

**Documentación de referencia:** a fin de obtener información sobre los métodos más importantes disponibles en el SDK de transmisión de iOS de Amazon IVS, consulte la documentación de referencia en [https://aws.github.io/amazon-ivs-broadcast-docs/1.40.0/ios/](https://aws.github.io/amazon-ivs-broadcast-docs/1.40.0/ios/).

**Código de muestra:** Consulte el repositorio de muestra de iOS en GitHub: [https://github.com/aws-samples/amazon-ivs-real-time-streaming-ios-samples](https://github.com/aws-samples/amazon-ivs-real-time-streaming-ios-samples).

**Requisitos de la plataforma:** iOS 14\$1

# Introducción al SDK de transmisión para iOS de IVS \$1 Transmisión en tiempo real
<a name="broadcast-ios-getting-started"></a>

Este documento explica los pasos necesarios para comenzar a utilizar el SDK de transmisión para iOS de la transmisión en tiempo real de IVS.

## Instalación de la biblioteca
<a name="broadcast-ios-install"></a>

Recomendamos que integre el SDK de transmisión a través de Swift Package Manager. (También puede agregar el marco a su proyecto de forma manual).

### Recomendación: integre el SDK de transmisión (Swift Package Manager)
<a name="broadcast-ios-install-swift"></a>

1. Descargue el archivo Package.swift desde [https://broadcast.live-video.net/1.40.0/Package.swift](https://broadcast.live-video.net/1.40.0/Package.swift).

1. En su proyecto, cree un nuevo directorio denominado AmazonIVSBroadcast y agréguelo al control de versiones.

1. Coloque el archivo Package.swift descargado en el nuevo directorio.

1. En Xcode, vaya a **Archivo > Agregar dependencias de paquete** y seleccione **Agregar local…**.

1. Navegue hasta el directorio AmazonIVSBroadcast que ha creado, selecciónelo y elija **Agregar paquete**.

1. Cuando se le pida que **elija productos de paquete para AmazonIVSBroadcast**, seleccione **AmazonIVSBroadcastStages** como su **producto de paquete** mediante la configuración del destino de la aplicación en la sección **Agregar a destino**.

1. Seleccione **Agregar paquete**.

**Importante**: el SDK de transmisión para transmisión en tiempo real de IVS incluye todas las características del SDK de transmisión para transmisión de baja latencia de IVS. No es posible integrar ambos SDK en el mismo proyecto.

### Método alternativo: instalar el marco de forma manual
<a name="broadcast-ios-install-manual"></a>

1. Descargue la versión más reciente desde [https://broadcast.live-video.net/1.40.0/AmazonIVSBroadcast-Stages.xcframework.zip](https://broadcast.live-video.net/1.40.0/AmazonIVSBroadcast-Stages.xcframework.zip).

1. Extraiga el contenido del archivo. `AmazonIVSBroadcast.xcframework` contiene el SDK para el dispositivo y el simulador.

1. Integre el `AmazonIVSBroadcast.xcframework` arrastrándolo a la sección **Frameworks, Libraries, and Embedded Content (Marcos, bibliotecas y contenido integrado)** de la pestaña **General** para el destino de la aplicación.  
![\[La sección Marcos, librerías y contenido integrado de la pestaña General para el destino de la aplicación.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/iOS_Broadcast_SDK_Guide_xcframework.png)

## Solicitar permisos
<a name="broadcast-ios-permissions"></a>

La aplicación debe solicitar permiso para acceder a la cámara y al micrófono del usuario. (Esto no es específico de Amazon IVS; es necesario para cualquier aplicación que necesite acceso a cámaras y micrófonos).

Aquí, verificamos si el usuario ya ha concedido permisos y, de no ser así, preguntamos por ellos:

```
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized: // permission already granted.
case .notDetermined:
   AVCaptureDevice.requestAccess(for: .video) { granted in
       // permission granted based on granted bool.
   }
case .denied, .restricted: // permission denied.
@unknown default: // permissions unknown.
}
```

Es necesario hacer esto para los tipos de medios `.video` y `.audio`, si desea tener acceso a cámaras y micrófonos, respectivamente.

También es necesario agregar entradas para `NSCameraUsageDescription` y `NSMicrophoneUsageDescription` para su `Info.plist`. De lo contrario, la aplicación se bloqueará al intentar solicitar permisos.

## Desactivar el temporizador inactivo de la aplicación
<a name="broadcast-ios-disable-idle-timer"></a>

Esto es opcional, pero recomendable. Evita que el dispositivo se ponga en suspensión mientras utiliza el SDK de transmisión, lo que interrumpiría la transmisión.

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

# Publicación y suscripción con el SDK de transmisión para iOS de IVS \$1 Transmisión en tiempo real
<a name="ios-publish-subscribe"></a>

Este documento explica los pasos necesarios para publicar y suscribirse a una fase mediante el SDK de transmisión para iOS de la transmisión en tiempo real de IVS.

## Conceptos
<a name="ios-publish-subscribe-concepts"></a>

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

### Etapa
<a name="ios-publish-subscribe-concepts-stage"></a>

La clase `IVSStage` es el principal punto de interacción entre la aplicación host y el SDK. La clase representa el escenario como tal y se usa para entrar y salir de él. Para crear un escenario o incorporarse a él, es necesaria una cadena de símbolos válida y que no haya vencido del plano de control (representada como `token`). Entrar a un escenario y salir de él es sencillo.

```
let stage = try IVSStage(token: token, strategy: self)

try stage.join()

stage.leave()
```

También se pueden adjuntar `IVSStageRenderer` y `IVSErrorDelegate` en la clase `IVSStage`:

```
let stage = try IVSStage(token: token, strategy: self)
stage.errorDelegate = self
stage.addRenderer(self) // multiple renderers can be added
```

### Strategy (Estrategia)
<a name="ios-publish-subscribe-concepts-strategy"></a>

El protocolo `IVSStageStrategy` proporciona una forma para que la aplicación host comunique el estado deseado del escenario al SDK. Es necesario implementar las siguientes tres funciones: `shouldSubscribeToParticipant`, `shouldPublishParticipant` y `streamsToPublishForParticipant`. Todas se analizan a continuación.

#### Suscripción a participantes
<a name="ios-publish-subscribe-concepts-strategy-participants"></a>

```
func stage(_ stage: IVSStage, shouldSubscribeToParticipant participant: IVSParticipantInfo) -> IVSStageSubscribeType
```

Cuando un participante remoto se une a un escenario, el SDK consulta a la aplicación host sobre el estado de suscripción deseado para ese participante. Las opciones son `.none`, `.audioOnly` y `.audioVideo`. Al devolver un valor para esta función, la aplicación host no tiene que preocuparse por el estado de la publicación, el estado actual de la suscripción ni el estado de la conexión del escenario. Si se devuelve `.audioVideo`, el SDK espera a que el participante remoto publique antes de suscribirse y actualiza la aplicación host a través del renderizador durante todo el proceso.

Este es un ejemplo de implementación:

```
func stage(_ stage: IVSStage, shouldSubscribeToParticipant participant: IVSParticipantInfo) -> IVSStageSubscribeType {
    return .audioVideo
}
```

Esta es la implementación completa de esta función para una aplicación host que siempre quiere que todos los participantes se vean entre sí; por ejemplo, una aplicación de videochat.

También son posibles implementaciones más avanzadas. Utilice la propiedad `attributes` en `IVSParticipantInfo` para suscribirse de forma selectiva a los participantes en función de los atributos proporcionados por el servidor:

```
func stage(_ stage: IVSStage, shouldSubscribeToParticipant participant: IVSParticipantInfo) -> IVSStageSubscribeType {
    switch participant.attributes["role"] {
    case "moderator": return .none
    case "guest": return .audioVideo
    default: return .none
    }
}
```

Esto se puede utilizar para crear un escenario en el que los moderadores puedan monitorear a todos los invitados sin que ellos mismos los vean ni los escuchen. La aplicación host podría utilizar una lógica empresarial adicional para permitir que los moderadores se vean entre sí, pero permanezcan invisibles para los invitados.

#### Configuración de la suscripción a participantes
<a name="ios-publish-subscribe-concepts-strategy-participants-config"></a>

```
func stage(_ stage: IVSStage, subscribeConfigurationForParticipant participant: IVSParticipantInfo) -> IVSSubscribeConfiguration
```

Si se está subscribiendo a un participante remoto (consulte [Suscripción a participantes](#ios-publish-subscribe-concepts-strategy-participants)), el SDK consulta a la aplicación host sobre una configuración de la subscrición personalizada para ese participante. Esta configuración es opcional y permite a la aplicación host controlar determinados aspectos del comportamiento de los suscriptores. Para obtener información sobre lo que se puede configurar, consulte [SubscribeConfiguration](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/interfaces/SubscribeConfiguration) en la documentación de referencia del SDK.

Este es un ejemplo de implementación:

```
func stage(_ stage: IVSStage, subscribeConfigurationForParticipant participant: IVSParticipantInfo) -> IVSSubscribeConfiguration {
    let config = IVSSubscribeConfiguration()

    try! config.jitterBuffer.setMinDelay(.medium())

    return config
}
```

Esta implementación actualiza el retraso mínimo del búfer de fluctuación para todos los participantes suscritos al valor preestablecido `MEDIUM`.

Como en el caso de `shouldSubscribeToParticipant`, también son posibles implementaciones más avanzadas. El valor de `ParticipantInfo` proporcionado se puede utilizar para actualizar selectivamente la configuración de suscripción para participantes específicos.

Se recomienda utilizar los comportamientos predeterminados. Especifique la configuración personalizada solo si hay un comportamiento en particular que desee cambiar.

#### Publicación
<a name="ios-publish-subscribe-concepts-strategy-publishing"></a>

```
func stage(_ stage: IVSStage, shouldPublishParticipant participant: IVSParticipantInfo) -> Bool
```

Una vez realizada la conexión al escenario, el SDK consulta la aplicación host para ver si un participante en particular tiene que publicar. Esto solo se invoca en los participantes locales que tienen permiso para publicar en función del token proporcionado.

Este es un ejemplo de implementación:

```
func stage(_ stage: IVSStage, shouldPublishParticipant participant: IVSParticipantInfo) -> Bool {
    return true
}
```

Esto es para una aplicación de videochat estándar en la que los usuarios siempre quieren publicar. Pueden activar y desactivar el sonido y el video para ocultarse o verse y escucharse al instante. (También pueden usar la opción de publicar o anular la publicación, pero esto es mucho más lento. Es preferible silenciar o activar el sonido en casos de uso en los que se quiera cambiar la visibilidad de manera frecuente).

#### Elección de las transmisiones que publicar
<a name="ios-publish-subscribe-concepts-strategy-streams"></a>

```
func stage(_ stage: IVSStage, streamsToPublishForParticipant participant: IVSParticipantInfo) -> [IVSLocalStageStream]
```

Al publicar, esto se utiliza para determinar qué transmisiones de audio y video se tienen que publicar. Esto se explica en mayor detalle más adelante en [Publicación de una transmisión multimedia](#ios-publish-subscribe-publish-stream).

#### Actualización de la estrategia
<a name="ios-publish-subscribe-concepts-strategy-updates"></a>

La estrategia pretende ser dinámica: los valores devueltos por cualquiera de las funciones anteriores se pueden cambiar en cualquier momento. Por ejemplo, si la aplicación host no quiere publicar hasta que el usuario final presione un botón, puede devolver una variable de `shouldPublishParticipant` (como `hasUserTappedPublishButton`). Cuando esa variable cambie en función de una interacción del usuario final, llame a `stage.refreshStrategy()` para indicar al SDK que debe consultar la estrategia a fin de obtener los valores más recientes y aplicar solo los cambios. Si el SDK observa que el valor `shouldPublishParticipant` cambió, iniciará el proceso de publicación. Si las consultas del SDK y todas las funciones devuelven el mismo valor que antes, la llamada `refreshStrategy` no hará ninguna modificación en el escenario.

Si el valor devuelto de `shouldSubscribeToParticipant` cambia de `.audioVideo` a `.audioOnly`, la transmisión de video se eliminará para todos los participantes con valores devueltos modificados, si ya existía con anterioridad una transmisión de video.

Por lo general, el escenario utiliza la estrategia para aplicar de la manera más eficiente la diferencia entre las estrategias anteriores y actuales, sin que la aplicación host tenga que preocuparse por todo el estado necesario para administrarla correctamente. Por eso, piense en la llamada a `stage.refreshStrategy()` como una operación barata, porque no hace nada a no ser que cambie la estrategia.

### Renderer
<a name="ios-publish-subscribe-concepts-renderer"></a>

El protocolo `IVSStageRenderer` comunica el estado del escenario a la aplicación host. Por lo general, los eventos proporcionados por el renderizador permiten el funcionamiento completo de las actualizaciones de la interfaz de usuario de la aplicación host. El renderizador ofrece el siguiente resultado:

```
func stage(_ stage: IVSStage, participantDidJoin participant: IVSParticipantInfo)

func stage(_ stage: IVSStage, participantDidLeave participant: IVSParticipantInfo)

func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChange publishState: IVSParticipantPublishState)

func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChange subscribeState: IVSParticipantSubscribeState)

func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didAdd streams: [IVSStageStream])

func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didRemove streams: [IVSStageStream])

func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChangeMutedStreams streams: [IVSStageStream])

func stage(_ stage: IVSStage, didChange connectionState: IVSStageConnectionState, withError error: Error?)

func stage(_ stage: IVSStage, participant: IVSParticipantInfo, stream: IVSRemoteStageStream, didChangeStreamAdaption adaption: Bool)

func stage(_ stage: IVSStage, participant: IVSParticipantInfo, stream: IVSRemoteStageStream, didChange layers: [IVSRemoteStageStreamLayer])

func stage(_ stage: IVSStage, participant: IVSParticipantInfo, stream: IVSRemoteStageStream, didSelect layer: IVSRemoteStageStreamLayer?, reason: IVSRemoteStageStream.LayerSelectedReason)
```

No se espera que la información que proporciona el renderizador afecte a los valores de retorno de la estrategia. Por ejemplo, no se espera que el valor devuelto de `shouldSubscribeToParticipant` cambie cuando se llama a `participant:didChangePublishState`. Si la aplicación host quiere suscribirse a un participante en particular, debe devolver el tipo de suscripción deseado, independientemente del estado de publicación de ese participante. El SDK es responsable de garantizar que se aplique el estado deseado de la estrategia en el momento correcto según el estado del escenario.

Tenga en cuenta que solo los participantes que publican desencadenan `participantDidJoin`. `participantDidLeave` se desencadena cada vez que un participante deja de publicar o abandona la sesión del escenario.

## Publicación de una transmisión multimedia
<a name="ios-publish-subscribe-publish-stream"></a>

Los dispositivos locales, como las cámaras y los micrófonos integrados, se detectan a través de `IVSDeviceDiscovery`. A continuación, se muestra un ejemplo de cómo seleccionar la cámara frontal y el micrófono predeterminados y devolverlos como `IVSLocalStageStreams` para que los publique el SDK:

```
let devices = IVSDeviceDiscovery().listLocalDevices()

// Find the camera virtual device, choose the front source, and create a stream
let camera = devices.compactMap({ $0 as? IVSCamera }).first!
let frontSource = camera.listAvailableInputSources().first(where: { $0.position == .front })!
camera.setPreferredInputSource(frontSource)
let cameraStream = IVSLocalStageStream(device: camera)

// Find the microphone virtual device and create a stream
let microphone = devices.compactMap({ $0 as? IVSMicrophone }).first!
let microphoneStream = IVSLocalStageStream(device: microphone)

// Configure the audio manager to use the videoChat preset, which is optimized for bi-directional communication, including echo cancellation.
IVSStageAudioManager.sharedInstance().setPreset(.videoChat)

// This is a function on IVSStageStrategy
func stage(_ stage: IVSStage, streamsToPublishForParticipant participant: IVSParticipantInfo) -> [IVSLocalStageStream] {
    return [cameraStream, microphoneStream]
}
```

## Visualización y eliminación de participantes
<a name="ios-publish-subscribe-participants"></a>

Cuando se complete la suscripción, recibirá una matriz de objetos `IVSStageStream` a través de la función `didAddStreams` del renderizador. Para obtener una vista previa o recibir estadísticas del audio de este participante, puede acceder al objeto `IVSDevice` subyacente desde la transmisión:

```
if let imageDevice = stream.device as? IVSImageDevice {
    let preview = imageDevice.previewView()
    /* attach this UIView subclass to your view */
} else if let audioDevice = stream.device as? IVSAudioDevice {
    audioDevice.setStatsCallback( { stats in
        /* process stats.peak and stats.rms */
    })
}
```

Cuando un participante deja de publicar o anula su suscripción, se invoca la función `didRemoveStreams` con las transmisiones que se eliminaron. Las aplicaciones host tienen que usar esto como una señal para eliminar la transmisión de video del participante de la jerarquía de visualización.

`didRemoveStreams` se invoca para todas las situaciones en las que se puede eliminar una transmisión, como las siguientes:
+ El participante remoto deja de publicar.
+ Un dispositivo local cancela la suscripción o cambia la suscripción de `.audioVideo` a `.audioOnly`.
+ El participante remoto abandona el escenario.
+ El participante local abandona el escenario.

Ya que `didRemoveStreams` se invoca en todas las situaciones, no es necesaria una lógica empresarial personalizada para eliminar a los participantes de la interfaz de usuario durante las operaciones de licencia remota o local.

## Activación y desactivación del sonido de las transmisiones multimedia
<a name="ios-publish-subscribe-mute-streams"></a>

Los objetos `IVSLocalStageStream` tienen una función `setMuted` que controla si la transmisión está silenciada. Esta función se puede invocar en la transmisión antes o después de que la devuelva la función de estrategia `streamsToPublishForParticipant`.

**Importante**: Si `streamsToPublishForParticipant` devuelve una nueva instancia de objeto de `IVSLocalStageStream` después de una llamada a `refreshStrategy`, el estado de sonido desactivado del nuevo objeto de transmisión se aplicará al escenario. Tenga cuidado al crear nuevas instancias `IVSLocalStageStream` para asegurarse de que se mantenga el estado de sonido desactivado que se espera.

## Monitoreo del estado de sonido desactivado en los contenidos multimedia del participante remoto
<a name="ios-publish-subscribe-mute-state"></a>

Cuando un participante cambia el estado de sonido desactivado de su transmisión de video o audio, se invoca la función `didChangeMutedStreams` de renderizado con una lista de las transmisiones que cambiaron. Use la propiedad `isMuted` en `IVSStageStream` para actualizar su interfaz de usuario según corresponda:

```
func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChangeMutedStreams streams: [IVSStageStream]) {
    streams.forEach { stream in 
        /* stream.isMuted */
    }
}
```

## Creación de una configuración de escenario
<a name="ios-publish-subscribe-stage-config"></a>

Para personalizar los valores de la configuración de video de un escenario, utilice `IVSLocalStageStreamVideoConfiguration`:

```
let config = IVSLocalStageStreamVideoConfiguration()
try config.setMaxBitrate(900_000)
try config.setMinBitrate(100_000)
try config.setTargetFramerate(30)
try config.setSize(CGSize(width: 360, height: 640))
config.degradationPreference = .balanced
```

## Obtención de estadísticas de WebRTC
<a name="ios-publish-subscribe-webrtc-stats"></a>

Para obtener las estadísticas más recientes de WebRTC de una transmisión de publicación o de suscripción, utilice `requestRTCStats` en `IVSStageStream`. Cuando se complete una recopilación, recibirá estadísticas a través de `IVSStageStreamDelegate`, que se puede configurar en `IVSStageStream`. Para recopilar continuamente estadísticas de WebRTC, llame a esta función en un `Timer`.

```
func stream(_ stream: IVSStageStream, didGenerateRTCStats stats: [String : [String : String]]) {
    for stat in stats {
      for member in stat.value {
         print("stat \(stat.key) has member \(member.key) with value \(member.value)")
      }
   }
}
```

## Obtención de los atributos de los participantes
<a name="ios-publish-subscribe-participant-attributes"></a>

Si especifica atributos en la solicitud de la operación de `CreateParticipantToken`, podrá ver los atributos en las propiedades de `IVSParticipantInfo`:

```
func stage(_ stage: IVSStage, participantDidJoin participant: IVSParticipantInfo) {
    print("ID: \(participant.participantId)")
    for attribute in participant.attributes {
        print("attribute: \(attribute.key)=\(attribute.value)")
    }
}
```

## Mensajes incrustados
<a name="ios-publish-subscribe-embed-messages"></a>

El método `embedMessage` en IVSImageDevice permite insertar cargas útiles de metadatos directamente en los fotogramas de video durante la publicación. Esto activa la mensajería sincronizada por fotogramas para aplicaciones en tiempo real. La incrustación de mensajes solo está disponible cuando se utiliza el SDK para la publicación en tiempo real (no para la publicación de baja latencia).

No se garantiza que los mensajes incrustados lleguen a los suscriptores porque se incrustan directamente en los fotogramas de video y se transmiten a través de UDP, lo que no asegura la entrega de paquetes. La pérdida de paquetes durante la transmisión puede provocar la pérdida de mensajes, especialmente en condiciones de red deficientes. Para mitigar esta situación, el método `embedMessage` incluye un parámetro `repeatCount` que duplica el mensaje en varios fotogramas consecutivos, lo que aumenta la fiabilidad de la entrega. Esta capacidad solo está disponible para transmisiones de video.

### Uso de embedMessage
<a name="ios-embed-messages-using-embedmessage"></a>

Los clientes de publicación pueden incrustar cargas útiles de mensajes en su transmisión de video mediante el método `embedMessage` de IVSImageDevice. El tamaño de la carga útil debe ser superior a 0 KB e inferior a 1 KB. El número de mensajes incrustados insertados por segundo no debe superar los 10 KB por segundo.

```
let imageDevice: IVSImageDevice = imageStream.device as! IVSImageDevice
let messageData = Data("hello world".utf8)

do {
    try imageDevice.embedMessage(messageData, withRepeatCount: 0)
} catch {
    print("Failed to embed message: \(error)")
}
```

### Cargas útiles de mensaje que se repiten
<a name="ios-embed-messages-repeat-payloads"></a>

Utilice `repeatCount` para duplicar el mensaje en varios marcos a fin de mejorar la fiabilidad. Este valor debe estar entre 0 y 30. Los clientes receptores deben tener una lógica para desduplicar el mensaje.

```
try imageDevice.embedMessage(messageData, withRepeatCount: 5)

// repeatCount: 0-30, receiving clients should handle duplicates
```

### Lectura de mensajes incrustados
<a name="ios-embed-messages-read-messages"></a>

Consulte la sección “Obtención de información de mejora adicional (SEI)” a continuación para saber cómo leer los mensajes incrustados de las transmisiones entrantes. 

## Obtención de información de mejora adicional (SEI)
<a name="ios-publish-subscribe-sei-attributes"></a>

La unidad NAL de información de mejora adicional (SEI) se usa para almacenar metadatos alineados con los fotogramas junto con el video. Los clientes suscriptores pueden leer las cargas útiles SEI de un publicador que publica un video H.264 mediante la inspección de la propiedad `embeddedMessages` en los objetos `IVSImageDeviceFrame` que salen del `IVSImageDevice` del publicador. Para ello, adquiera el `IVSImageDevice` de un publicador y, a continuación, observe cada fotograma a través de una devolución de llamada proporcionada a `setOnFrameCallback`, como se muestra en el siguiente ejemplo:

```
// in an IVSStageRenderer’s stage:participant:didAddStreams: function, after acquiring the new IVSImageStream

let imageDevice: IVSImageDevice? = imageStream.device as? IVSImageDevice
imageDevice?.setOnFrameCallback { frame in
	for message in frame.embeddedMessages {
    		if let seiMessage = message as? IVSUserDataUnregisteredSEIMessage {
        		let seiMessageData = seiMessage.data
        		let seiMessageUUID = seiMessage.UUID

        		// interpret the message's data based on the UUID
    		}
	}
}
```

## Continuación de la sesión en segundo plano
<a name="ios-publish-subscribe-background-session"></a>

Cuando la aplicación pasa a segundo plano, puede seguir en el escenario mientras escucha el audio remoto, aunque no podrá enviar su propia imagen y audio. Es necesario que actualice la implementación `IVSStrategy` para detener la publicación y se suscriba a `.audioOnly` (o a `.none`, si corresponde):

```
func stage(_ stage: IVSStage, shouldPublishParticipant participant: IVSParticipantInfo) -> Bool {
    return false
}
func stage(_ stage: IVSStage, shouldSubscribeToParticipant participant: IVSParticipantInfo) -> IVSStageSubscribeType {
    return .audioOnly
}
```

A continuación, haga una llamada a `stage.refreshStrategy()`.

## Codificación por capas con la transmisión simultánea
<a name="ios-publish-subscribe-layered-encoding-simulcast"></a>

La codificación por capas con transmisión simultánea es una característica de transmisión en tiempo real de IVS que permite a quienes publican enviar múltiples capas de video con diferentes niveles de calidad, mientras que los suscriptores pueden configurar estas capas de manera dinámica o manual. Esta característica se describe con más detalle en el documento [Optimizaciones de transmisión](real-time-streaming-optimization.md).

### Configuración de la codificación por capas (publicador)
<a name="ios-layered-encoding-simulcast-configure-publisher"></a>

Como publicador, para habilitar la codificación por capas con transmisión simultánea, agregue la siguiente configuración a `IVSLocalStageStream` en la instanciación:

```
// Enable Simulcast
let config = IVSLocalStageStreamVideoConfiguration()
config.simulcast.enabled = true

let cameraStream = IVSLocalStageStream(device: camera, configuration: config)

// Other Stage implementation code
```

En función de la resolución establecida en la configuración de video, se codificará y enviará una cantidad determinada de capas, tal como se define en la sección [Capas, calidades y velocidades de fotogramas predeterminadas](real-time-streaming-optimization.md#real-time-streaming-optimization-default-layers) de *Optimizaciones de transmisión*.

Además, si lo desea, puede configurar capas individuales desde la configuración de transmisión simultánea:

```
// Enable Simulcast
let config = IVSLocalStageStreamVideoConfiguration()
config.simulcast.enabled = true

let layers = [
    IVSStagePresets.simulcastLocalLayer().default720(),
    IVSStagePresets.simulcastLocalLayer().default180()
]

try config.simulcast.setLayers(layers)

let cameraStream = IVSLocalStageStream(device: camera, configuration: config)

// Other Stage implementation code
```

De manera opcional, también puede crear sus propias configuraciones de capas personalizadas para hasta tres capas. Si proporciona una matriz vacía o ningún valor, se utilizan los valores predeterminados descritos con anterioridad. Las capas se describen con los siguientes setters de propiedades obligatorios:
+ `setSize: CGSize;`
+ `setMaxBitrate: integer;`
+ `setMinBitrate: integer;`
+ `setTargetFramerate: float;`

A partir de los ajustes preestablecidos, puede anular las propiedades individuales o crear una configuración completamente nueva:

```
// Enable Simulcast
let config = IVSLocalStageStreamVideoConfiguration()
config.simulcast.enabled = true

let customHiLayer = IVSStagePresets.simulcastLocalLayer().default720()
try customHiLayer.setTargetFramerate(15)

let layers = [
    customHiLayer,
    IVSStagePresets.simulcastLocalLayer().default180()
]

try config.simulcast.setLayers(layers)

let cameraStream = IVSLocalStageStream(device: camera, configuration: config)

// Other Stage implementation code
```

Para conocer los valores máximos, los límites y los errores que se pueden activar al configurar capas individuales, consulta la documentación de referencia del SDK.

### Configuración de la codificación por capas (suscriptor)
<a name="ios-layered-encoding-simulcast-configure-subscriber"></a>

Como suscriptor, no es necesario hacer nada para habilitar la codificación por capas. Si un publicador envía capas de transmisión simultánea, el servidor, de forma predeterminada, se adaptará dinámicamente entre las capas para elegir la calidad óptima en función de las condiciones de la red y del dispositivo del suscriptor.

Como alternativa, existen varias opciones para seleccionar las capas explícitas que envía el publicador, tal y como se describe a continuación.

### Opción 1: preferencia de calidad de la capa inicial
<a name="ios-layered-encoding-simulcast-layer-quality-preference"></a>

Mediante la estrategia `subscribeConfigurationForParticipant`, es posible elegir qué capa inicial desea recibir como suscriptor:

```
func stage(_ stage: IVSStage, subscribeConfigurationForParticipant participant: IVSParticipantInfo) -> IVSSubscribeConfiguration {
    let config = IVSSubscribeConfiguration()

    config.simulcast.initialLayerPreference = .lowestQuality

    return config
}
```

De forma predeterminada, a los suscriptores siempre se les envía primero la capa de calidad más baja; poco a poco se incrementa hasta llegar a la capa de calidad más alta. Esto optimiza el consumo de ancho de banda del usuario final y proporciona el mejor tiempo de inicio del video, lo cual reduce las congelaciones iniciales de video para los usuarios en redes más débiles.

Estas opciones se encuentran disponibles para `InitialLayerPreference`:
+ `lowestQuality`: el servidor entrega primero la capa de video de menor calidad. Esto optimiza tanto el consumo de ancho de banda como el tiempo de inicio del contenido multimedia. La calidad se define como la combinación de tamaño, velocidad de bits y velocidad de fotogramas del video. Por ejemplo, un video 720p es de menor calidad que un video 1080p.
+ `highestQuality`: el servidor entrega primero la capa de video de mayor calidad. Esto optimiza la calidad, pero puede aumentar el tiempo de inicio del contenido multimedia. La calidad se define como la combinación de tamaño, velocidad de bits y velocidad de fotogramas del video. Por ejemplo, el video 1080p es de mayor calidad que el video 720p.

**Nota:** Para que se apliquen las preferencias de la capa inicial (la llamada `initialLayerPreference`), es necesario volver a suscribirse, ya que estas actualizaciones no se aplican a la suscripción activa.

### Opción 2: capa preferida para la transmisión
<a name="ios-layered-encoding-simulcast-preferred-layer"></a>

El método de estrategia `preferredLayerForStream` le permite seleccionar una capa después de que la transmisión haya comenzado. Este método de estrategia recibe la información del participante y de la transmisión, por lo que puede seleccionar una capa para cada participante de forma individual. El SDK llama a este método en respuesta a eventos específicos, como cuando cambian las capas de transmisión o el estado del participante o la aplicación host actualiza la estrategia.

El método de estrategia devuelve un objeto `IVSRemoteStageStreamLayer`, que puede ser uno de los siguientes:
+ Un objeto de capa, como uno devuelto por `IVSRemoteStageStream.layers`.
+ nulo, que indica que no se debe seleccionar ninguna capa y que se prefiere la adaptación dinámica.

Por ejemplo, la siguiente estrategia hará que los usuarios seleccionen siempre la capa de video de menor calidad disponible:

```
func stage(_ stage: IVSStage, participant: IVSParticipantInfo, preferredLayerFor stream: IVSRemoteStageStream) -> IVSRemoteStageStreamLayer? {
    return stream.lowestQualityLayer
}
```

Para restablecer la selección de capas y volver a la adaptación dinámica, devuelva nulo o sin definir en la estrategia. En este ejemplo, `appState` es una variable de marcador de posición que representa el estado de la aplicación host.

```
func stage(_ stage: IVSStage, participant: IVSParticipantInfo, preferredLayerFor stream: IVSRemoteStageStream) -> IVSRemoteStageStreamLayer? {
    If appState.isAutoMode {
        return nil
    } else {
        return appState.layerChoice
    }
}
```

### Opción 3: ayudantes de la capa RemoteStageStream
<a name="ios-layered-encoding-simulcast-remotestagestream-helpers"></a>

`IVSRemoteStageStream` cuenta con varios ayudantes que se pueden utilizar para tomar decisiones sobre la selección de capas y mostrar las selecciones correspondientes a los usuarios finales:
+ **Eventos de capa**: además de `IVSStageRenderer`, `IVSRemoteStageStreamDelegate` cuenta con eventos que comunican los cambios de adaptación de capas y transmisión simultánea:
  + `func stream(_ stream: IVSRemoteStageStream, didChangeAdaption adaption: Bool)`
  + `func stream(_ stream: IVSRemoteStageStream, didChange layers: [IVSRemoteStageStreamLayer])`
  + `func stream(_ stream: IVSRemoteStageStream, didSelect layer: IVSRemoteStageStreamLayer?, reason: IVSRemoteStageStream.LayerSelectedReason)`
+ **Métodos de capa**: `IVSRemoteStageStream` tiene varios métodos de ayudante que se pueden utilizar para obtener información sobre la transmisión y las capas que se presentan. Estos métodos están disponibles en la transmisión remota proporcionada en la estrategia `preferredLayerForStream`, así como en las transmisiones remotas expuestas a través de `func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didAdd streams: [IVSStageStream])`.
  + `stream.layers`
  + `stream.selectedLayer`
  + `stream.lowestQualityLayer`
  + `stream.highestQualityLayer`
  + `stream.layers(with: IVSRemoteStageStreamLayerConstraints)`

Para conocer los detalles, consulte la clase `IVSRemoteStageStream` en la [documentación de referencia del SDK](https://aws.github.io/amazon-ivs-broadcast-docs/latest/ios/). En cuanto a la razón `LayerSelected`, si se devuelve `UNAVAILABLE`, esto indica que no se ha podido seleccionar la capa solicitada. En su lugar, se selecciona la mejor opción posible, que suele ser una capa de menor calidad para mantener la estabilidad de la transmisión.

## Transmisión del escenario a un canal de IVS
<a name="ios-publish-subscribe-broadcast-stage"></a>

Para transmitir un escenario, cree una `IVSBroadcastSession` independiente y, a continuación, siga las instrucciones habituales para la transmisión con el SDK, descritas con anterioridad. La propiedad `device` en `IVSStageStream` será un `IVSImageDevice` o `IVSAudioDevice`, tal y como se muestra en el fragmento anterior. Estos se pueden conectar a `IVSBroadcastSession.mixer` para transmitir todo el escenario en un diseño personalizable.

Si lo desea, puede componer un escenario y transmitirlo a un canal de IVS de baja latencia para llegar a un público más amplio. Consulte [Habilitación de varios hosts en una transmisión de Amazon IVS](https://docs.aws.amazon.com//ivs/latest/LowLatencyUserGuide/multiple-hosts.html) en la Guía del usuario de transmisión de baja latencia.

# Cómo elige iOS la resolución de la cámara y la velocidad de fotogramas
<a name="ios-publish-subscribe-resolution-framerate"></a>

La cámara administrada por el SDK de transmisión optimiza su resolución y velocidad de fotogramas (fotogramas por segundo o FPS) para minimizar la producción de calor y el consumo de energía. En esta sección se explica de qué manera se seleccionan la resolución y la velocidad de fotogramas para ayudar a las aplicaciones de host a optimizar los casos de uso.

Al crear una `IVSLocalStageStream` a una `IVSCamera`, la cámara se optimiza para una velocidad de fotogramas `IVSLocalStageStreamVideoConfiguration.targetFramerate` y una resolución de `IVSLocalStageStreamVideoConfiguration.size`. Al llamar a `IVSLocalStageStream.setConfiguration` se actualiza la cámara con valores más recientes. 

## Vista previa de cámara
<a name="resolution-framerate-camera-preview"></a>

Si crea una vista previa de una `IVSCamera` sin adjuntarla a una `IVSBroadcastSession` o `IVSStage`, la resolución predeterminada será de 1080p y la velocidad de fotogramas de 60 fps.

## Transmisión de un escenario
<a name="resolution-framerate-broadcast-stage"></a>

Cuando se utiliza una `IVSBroadcastSession` para transmitir un `IVSStage`, el SDK intenta optimizar la cámara con una resolución y una velocidad de fotogramas que cumplan con los criterios de ambas sesiones.

Por ejemplo, si la configuración de transmisión está configurada para tener una velocidad de fotogramas de 15 FPS y una resolución de 1080p, mientras que el escenario tiene una velocidad de fotogramas de 30 FPS y una resolución de 720p, el SDK seleccionará una configuración de cámara con una velocidad de fotogramas de 30 FPS y una resolución de 1080p. La `IVSBroadcastSession` eliminará el resto de fotogramas de la cámara y `IVSStage` escalará la imagen de 1080p a 720p.

Si una aplicación host planea usar tanto `IVSBroadcastSession` como `IVSStage`, con una cámara, recomendamos que las propiedades de `targetFramerate` y `size` de las configuraciones respectivas coincidan. Si no coinciden, la cámara podría reconfigurase a sí misma al capturar video, lo que provocaría un breve retraso en la entrega de las muestras de video.

Si tener valores idénticos no cumple con los requisitos de uso de la aplicación host, crear primero la cámara de mayor calidad evitará que la cámara se reconfigure por sí sola cuando se agregue la sesión de menor calidad. Por ejemplo, si emite a 1080p y 30 FPS y, posteriormente, se une a un escenario configurado en 720p y 30 FPS, la cámara no se reconfigurará por sí sola y el video continuará sin interrupciones. Esto se debe a que 720p es inferior o igual a 1080p y 30 FPS es inferior o igual a 30 FPS.

## Velocidades de fotogramas, resoluciones y proporciones de aspecto arbitrarias
<a name="resolution-framerate-arbitrary"></a>

La mayoría del hardware de las cámaras puede coincidir exactamente con los formatos habituales, como 720p a 30 FPS o 1080p a 60 FPS. Sin embargo, no se puede hacer coincidir exactamente con todos los formatos. El SDK de transmisión elige la configuración de la cámara según las siguientes reglas (en orden de prioridad):

1. El ancho y el alto de la resolución son mayores o iguales a la resolución deseada, pero dentro de esta restricción, el ancho y el alto son lo más pequeños posible.

1. La velocidad de fotogramas es mayor o igual a la velocidad de fotogramas deseada, pero dentro de esta restricción, la velocidad de fotogramas es lo más baja posible.

1. La relación de aspecto coincide con la relación de aspecto deseada.

1. Si hay varios formatos coincidentes, se utiliza el formato con el mayor campo de visión.

A continuación, se incluyen dos ejemplos:
+ La aplicación host está intentando transmitir en 4K a 120 FPS. La cámara seleccionada solo admite 4K a 60 FPS o 1080p a 120 FPS. El formato seleccionado será 4K a 60 FPS, porque la regla de resolución tiene mayor prioridad que la regla de velocidad de fotogramas.
+ Se solicita una resolución irregular, 1910x1070. La cámara utilizará 1920x1080. *Tenga cuidado: si elige una resolución como 1921x1080, la cámara escalará verticalmente hasta la siguiente resolución disponible (por ejemplo, 2592x1944), lo que supone una penalización del ancho de banda de la memoria y de la CPU*.

## ¿Qué pasa con Android?
<a name="resolution-framerate-android"></a>

Android no ajusta la resolución ni la velocidad de fotogramas sobre la marcha como lo hace iOS, por lo que esto no afecta al SDK de transmisión de Android.

# Problemas conocidos y soluciones alternativas del SDK de transmisión para iOS de IVS \$1 Transmisión en tiempo real
<a name="broadcast-ios-known-issues"></a>

Este documento enumera problemas conocidos que puede experimentar al utilizar el SDK de transmisión para iOS de la transmisión en tiempo real de Amazon IVS y sugiere posibles soluciones alternativas.
+ Cambiar las rutas de audio Bluetooth puede ser impredecible. Si conecta un dispositivo nuevo a mitad de la sesión, iOS puede o no cambiar automáticamente la ruta de entrada. Además, no es posible elegir entre varios auriculares Bluetooth conectados al mismo tiempo. Esto ocurre tanto en las sesiones normales de transmisión como en las del escenario.

  **Solución alternativa:** si planea utilizar auriculares Bluetooth, conéctelos antes de iniciar la transmisión o el escenario y déjelos conectados durante toda la sesión.
+ Los participantes que utilicen un iPhone 14, iPhone 14 Plus, iPhone 14 Pro o iPhone 14 Pro Max pueden provocar problemas de eco en otros participantes.

  **Solución alternativa:** los participantes que utilicen los dispositivos afectados pueden utilizar auriculares para evitar que otros participantes sufran el problema de eco.
+ Cuando un participante se incorpora con un token que utiliza otro participante, la primera conexión se pierde sin que se produzca un error específico.

  **Solución alternativa:** ninguna.
+ Hay un problema poco frecuente en el que el editor publica, pero el estado de publicación que reciben los suscriptores es `inactive`.

  **Solución alternativa:** pruebe a salir de la sesión y, a continuación, reincorporarse. Si el problema persiste, cree un nuevo token para el publicador.
+ Cuando un participante publica o se suscribe, es posible recibir un error con el código 1400 que indica la desconexión debido a un problema de red, incluso cuando la red es estable.

  **Solución alternativa:** intente volver a publicar o a suscribirse.
+ Durante una sesión del escenario, se puede producir un problema intermitente y poco frecuente de distorsión de audio, por lo general en llamadas de mayor duración.

  **Solución alternativa:** el participante con audio distorsionado puede abandonar la sesión y volver a unirse a ella o anular la publicación y volver a publicar su audio para solucionar el problema.

# Control de errores en el SDK de transmisión para iOS de IVS \$1 Transmisión en tiempo real
<a name="broadcast-ios-error-handling"></a>

En esta sección se ofrece información general sobre las condiciones de error, cómo el SDK de transmisión para iOS las notifica a la aplicación y qué debe hacer una aplicación cuando se producen esos errores.

## Errores fatales frente a errores no fatales
<a name="broadcast-ios-fatal-vs-nonfatal-errors"></a>

El objeto de error tiene un booleano “es grave”. Se trata de una entrada de diccionario `IVSBroadcastErrorIsFatalKey` que contiene un booleano.

En general, los errores fatales están relacionados con la conexión al servidor de etapas (o bien no se puede establecer una conexión o bien se pierde y no se puede recuperar). La aplicación debe volver a crear el escenario y volver a unirse, posiblemente con un nuevo token o cuando se recupere la conectividad del dispositivo.

Los errores no fatales suelen estar relacionados con el estado de publicación/suscripción y son gestionados por el SDK, que reintenta la operación de publicación/suscripción.

Puede comprobar esta propiedad:

```
let nsError = error as NSError
if nsError.userInfo[IVSBroadcastErrorIsFatalKey] as? Bool == true {
  // the error is fatal
}
```

## Errores de unión
<a name="broadcast-ios-stage-join-errors"></a>

### Token con formato incorrecto
<a name="broadcast-ios-stage-join-errors-malformed-token"></a>

Esto ocurre cuando el token de la etapa tiene un formato incorrecto.

El SDK lanza una excepción de Swift con el código de error =1000 y IVSBroadcastErrorisFatalKey =YES.

**Acción**: cree un token válido e intente unirse de nuevo.

### Token vencido
<a name="broadcast-ios-stage-join-errors-expired-token"></a>

Esto ocurre cuando el token de la etapa está caducado.

El SDK lanza una excepción de Swift con el código de error =1001 y IVSBroadcastErrorisFatalKey =YES.

**Acción**: cree un token nuevo e intente unirse de nuevo.

### Token no válido o revocado
<a name="broadcast-ios-stage-join-errors-invalid-token"></a>

Esto ocurre cuando el token de la etapa no tiene un formato incorrecto, sino que el servidor de etapas lo rechaza. Esto se informa de forma asíncrona a través del renderizador de la etapa suministrado por la aplicación.

El SDK llama a `stage(didChange connectionState, withError error)` con el código de error =1026 y IVSBroadcastErrorisFatalKey =YES.

**Acción**: cree un token válido e intente unirse de nuevo.

### Errores de red durante la unión inicial
<a name="broadcast-ios-stage-join-errors-network-initial-join"></a>

Esto ocurre cuando el SDK no puede ponerse en contacto con el servidor de etapas para establecer una conexión. Esto se informa de forma asíncrona a través del renderizador de la etapa suministrado por la aplicación.

El SDK llama a `stage(didChange connectionState, withError error)` con el código de error =1300 y IVSBroadcastErrorisFatalKey =YES.

**Acción**: espere a que se recupere la conectividad del dispositivo e intente conectarse de nuevo.

### Errores de red cuando ya está conectado
<a name="broadcast-ios-stage-join-errors-network-already-joined"></a>

Si se interrumpe la conexión de red del dispositivo, es posible que el SDK pierda la conexión con los servidores de etapas. Esto se informa de forma asíncrona a través del renderizador de la etapa suministrado por la aplicación.

El SDK llama `stage(didChange connectionState, withError error)` con el código de error =1300 y el valor de IVSBroadcastErrorIsFatalKey =YES.

**Acción**: espere a que se recupere la conectividad del dispositivo e intente conectarse de nuevo.

## Errores de publicación/suscripción
<a name="broadcast-ios-publish-subscribe-errors"></a>

### Inicial
<a name="broadcast-ios-publish-subscribe-errors-initial"></a>

Existen varios errores:
+ MultihostSessionOfferCreationFailPublish (1020)
+ MultihostSessionOfferCreationFailSubscribe (1021)
+ MultihostSessionNoIceCandidates (1022)
+ MultihostSessionStageAtCapacity (1024)
+ SignallingSessionCannotRead (1201)
+ SignallingSessionCannotSend (1202)
+ SignallingSessionBadResponse (1203)

Estos se informan de forma asíncrona a través del renderizador de la etapa suministrado por la aplicación.

El SDK vuelve a intentar la operación un número limitado de veces. Durante los reintentos, el estado de publicación/suscripción es `ATTEMPTING_PUBLISH`/`ATTEMPTING_SUBSCRIBE`. Si el reintento se realiza correctamente, el estado cambia a `PUBLISHED`/`SUBSCRIBED`.

El SDK llama a `IVSErrorDelegate:didEmitError` con el código de error correspondiente y `IVSBroadcastErrorIsFatalKey == NO`.

**Acción**: no es necesario realizar ninguna acción, ya que el SDK vuelve a intentarlo automáticamente. Si lo desea, la aplicación puede actualizar la estrategia para forzar más reintentos.

### Ya está establecido, luego falla
<a name="broadcast-ios-publish-subscribe-errors-established"></a>

Una publicación o una suscripción pueden fallar una vez establecidas, muy probablemente debido a un error de red. El código de error para “Se ha perdido la conexión de pares debido a un error de red” es 1400.

Esto se informa de forma asíncrona a través del renderizador de la etapa suministrado por la aplicación.

El SDK vuelve a intentar la operación de publicación/suscripción. Durante los reintentos, el estado de publicación/suscripción es `ATTEMPTING_PUBLISH`/`ATTEMPTING_SUBSCRIBE`. Si el reintento se realiza correctamente, el estado cambia a `PUBLISHED`/`SUBSCRIBED`.

El SDK llama a `didEmitError` con el código de error =1400 y IVSBroadcastErrorIsFatalKey =NO.

**Acción**: no es necesario realizar ninguna acción, ya que el SDK vuelve a intentarlo automáticamente. Si lo desea, la aplicación puede actualizar la estrategia para forzar más reintentos. En caso de pérdida total de conectividad, es probable que la conexión a las etapas también falle.

# SDK de transmisión de IVS: dispositivos mixtos
<a name="broadcast-mixed-devices"></a>

Los dispositivos mixtos son dispositivos de audio y vídeo que reciben varias fuentes de entrada y generan una única salida. Los dispositivos mixtos son una potente característica que le permite definir y administrar varios elementos en pantalla (video) y pistas de audio. Puede combinar video y audio de varias fuentes, como cámaras, micrófonos, grabadoras de pantalla y audio y video generados por su aplicación. Puede utilizar transiciones para mover estas fuentes al video que transmite a IVS y agregarlas y eliminarlas a mitad de la transmisión.

Los dispositivos mixtos vienen en formatos de imagen y audio. Para crear un dispositivo de imagen mixtos, llame a:

`DeviceDiscovery.createMixedImageDevice()` en Android

`IVSDeviceDiscovery.createMixedImageDevice()` en iOS

El dispositivo devuelto se puede conectar a una `BroadcastSession` (transmisión de baja latencia) o `Stage` (transmisión en tiempo real), como cualquier otro dispositivo.

## Terminología
<a name="broadcast-mixed-devices-terminology"></a>

![\[Terminología de dispositivos mixtos de transmisión de IVS.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Broadcast_SDK_Mixer_Glossary.png)



| Plazo | Descripción | 
| --- | --- | 
| Dispositivo | Un componente de hardware o software que produce una entrada de audio o imagen. Algunos ejemplos de dispositivos son micrófonos, cámaras, auriculares Bluetooth y dispositivos virtuales, como grabadoras de pantalla o entradas de imágenes personalizadas. | 
| Dispositivo mixto | Un `Device` que se puede conectar a un `BroadcastSession` igual que cualquier otro `Device`, pero con API adicionales que permiten agregar objetos de `Source`. Los dispositivos mixtos tienen mezcladores internos que componen audio o imágenes, lo que produce un flujo de audio e imagen de salida único. Los dispositivos mixtos vienen en formatos de audio o imagen.  | 
| Configuración del dispositivo mixto | Un objeto de configuración para el dispositivo mixto. En el caso de los dispositivos de imágenes mixtos, configura propiedades como las dimensiones y la tasa de fotogramas. En el caso de los dispositivos de audio mixtos, esto configura el recuento de canales. | 
|  Origen | Un contenedor que define la posición de un elemento visual en la pantalla y las propiedades de una pista de audio en la mezcla de audio. Un dispositivo mixto se puede configurar con cero o más fuentes. Las fuentes tienen una configuración que afecta a la forma en que se utilizan los medios de la fuente. La imagen de arriba muestra cuatro fuentes de imagen: [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/broadcast-mixed-devices.html)  | 
| Configuración de fuente |  Un objeto de configuración para la fuente que se envía al dispositivo mixto. Los objetos de configuración completos se describen a continuación.   | 
| Transition | Para mover una ranura a una nueva posición o cambiar algunas de sus propiedades, utilice `MixedDevice.transitionToConfiguration()`. Este método recibe: [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/broadcast-mixed-devices.html) | 

## Dispositivo de audio mixto
<a name="broadcast-mixed-audio-device"></a>

### Configuración
<a name="broadcast-mixed-audio-device-configuration"></a>

`MixedAudioDeviceConfiguration` en Android

`IVSMixedAudioDeviceConfiguration` en iOS


| Nombre | Tipo | Descripción | 
| --- | --- | --- | 
| `channels` | Entero | Número de canales de salida del mezclador de audio. Valores válidos: 1, 2. 1 es audio mono; 2, audio estéreo. Valor predeterminado: 2. | 

### Configuración de fuente
<a name="broadcast-mixed-audio-device-source-configuration"></a>

`MixedAudioDeviceSourceConfiguration` en Android

`IVSMixedAudioDeviceSourceConfiguration` en iOS


| Nombre | Tipo | Descripción | 
| --- | --- | --- | 
| `gain` | Flotante | Ganancia de audio. Este es un multiplicador, por lo que cualquier valor superior a 1 aumenta la ganancia; cualquier valor inferior a 1, la disminuye. Valores válidos: 0-2. Valor predeterminado: 1.  | 

## Dispositivo de imagen mixto
<a name="broadcast-mixed-image-device"></a>

### Configuración
<a name="broadcast-mixed-image-device-configuration"></a>

`MixedImageDeviceConfiguration` en Android

`IVSMixedImageDeviceConfiguration` en iOS


| Nombre | Tipo | Descripción | 
| --- | --- | --- | 
| `size` | Vec2 | Tamaño del lienzo de video. | 
| `targetFramerate` | Entero | Número de fotogramas objetivo por segundo para el dispositivo mixto. En promedio, este valor debe alcanzarse, pero el sistema puede perder fotogramas en determinadas circunstancias (por ejemplo, uso elevado de CPU o carga elevada de la GPU). | 
| `transparencyEnabled` | Booleano | Esto permite la combinación mediante la propiedad `alpha` en las configuraciones de las fuentes de imagen. Configurar esto como `true` aumenta el consumo de memoria y CPU. Valor predeterminado: `false`. | 

### Configuración de fuente
<a name="broadcast-mixed-image-device-source-configuration"></a>

`MixedImageDeviceSourceConfiguration` en Android

`IVSMixedImageDeviceSourceConfiguration` en iOS


| Nombre | Tipo | Descripción | 
| --- | --- | --- | 
| `alpha` | Flotante | Alfa de la ranura. Esto es multiplicativo con cualquier valor alfa de la imagen. Valores válidos: 0-1, donde 0 es totalmente transparente y 1 es totalmente opaco. Valor predeterminado: 1. | 
| `aspect` | Modo de aspecto | Modo de relación de aspecto para cualquier imagen que se representa en la ranura. Valores válidos: [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/broadcast-mixed-devices.html) Valor predeterminado: `Fit`  | 
| `fillColor` | Vec4 | Color de relleno que se utilizará con `aspect Fit` cuando las relaciones de aspecto de la ranura y la imagen no coinciden. El formato es (rojo, verde, azul, alfa). Valor válido (para cada canal): 0-1. Valor predeterminado: (0, 0, 0, 0). | 
| `position` | Vec2 | Posición de la ranura (en píxeles), en relación con la esquina superior izquierda del lienzo. El origen de la ranura también está en la parte superior izquierda. | 
| `size` | Vec2 | Tamaño de la ranura (en píxeles). Establecer este valor también establece `matchCanvasSize` como `false`. Valor predeterminado: (0, 0); sin embargo, dado que el valor predeterminado de `matchCanvasSize` es `true`, el tamaño representado de la ranura es el tamaño del lienzo, y no (0, 0). | 
| `zIndex` | Flotante | Ordenación relativa de ranuras. Las ranuras con mayor valor de `zIndex` se dibujan encima de las ranuras con menor valor de `zIndex`. | 

## Creación y configuración de un dispositivo de imagen mixto
<a name="broadcast-mixed-image-device-creating-configuring"></a>

![\[Configuración de una sesión de transmisión de mezcla.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/Broadcast_SDK_Mixer_Configuring.png)


Aquí creamos una escena similar a la del principio de esta guía, con tres elementos en pantalla:
+ Ranura inferior izquierda para una cámara.
+ Ranura inferior derecha para una superposición de logotipo.
+ Ranura superior derecha para una película.

Tenga en cuenta que el origen del lienzo es la esquina superior izquierda, que es la misma que para las ranuras. Por lo tanto, colocar una ranura en (0, 0) la coloca en la esquina superior izquierda con toda la ranura visible.

### iOS
<a name="broadcast-mixed-image-device-creating-configuring-ios"></a>

```
let deviceDiscovery = IVSDeviceDiscovery()
let mixedImageConfig = IVSMixedImageDeviceConfiguration()
mixedImageConfig.size = CGSize(width: 1280, height: 720)
try mixedImageConfig.setTargetFramerate(60)
mixedImageConfig.isTransparencyEnabled = true
let mixedImageDevice = deviceDiscovery.createMixedImageDevice(with: mixedImageConfig)

// Bottom Left
let cameraConfig = IVSMixedImageDeviceSourceConfiguration()
cameraConfig.size = CGSize(width: 320, height: 180)
cameraConfig.position = CGPoint(x: 20, y: mixedImageConfig.size.height - cameraConfig.size.height - 20)
cameraConfig.zIndex = 2
let camera = deviceDiscovery.listLocalDevices().first(where: { $0 is IVSCamera }) as? IVSCamera
let cameraSource = IVSMixedImageDeviceSource(configuration: cameraConfig, device: camera)
mixedImageDevice.add(cameraSource)

// Top Right
let streamConfig = IVSMixedImageDeviceSourceConfiguration()
streamConfig.size = CGSize(width: 640, height: 320)
streamConfig.position = CGPoint(x: mixedImageConfig.size.width - streamConfig.size.width - 20, y: 20)
streamConfig.zIndex = 1
let streamDevice = deviceDiscovery.createImageSource(withName: "stream")
let streamSource = IVSMixedImageDeviceSource(configuration: streamConfig, device: streamDevice)
mixedImageDevice.add(streamSource)

// Bottom Right
let logoConfig = IVSMixedImageDeviceSourceConfiguration()
logoConfig.size = CGSize(width: 320, height: 180)
logoConfig.position = CGPoint(x: mixedImageConfig.size.width - logoConfig.size.width - 20,
                              y: mixedImageConfig.size.height - logoConfig.size.height - 20)
logoConfig.zIndex = 3
let logoDevice = deviceDiscovery.createImageSource(withName: "logo")
let logoSource = IVSMixedImageDeviceSource(configuration: logoConfig, device: logoDevice)
mixedImageDevice.add(logoSource)
```

### Android
<a name="broadcast-mixed-image-device-creating-configuring-android"></a>

```
val deviceDiscovery = DeviceDiscovery(this /* context */)
val mixedImageConfig = MixedImageDeviceConfiguration().apply {
    setSize(BroadcastConfiguration.Vec2(1280f, 720f))
    setTargetFramerate(60)
    setEnableTransparency(true)
}
val mixedImageDevice = deviceDiscovery.createMixedImageDevice(mixedImageConfig)

// Bottom Left
val cameraConfig = MixedImageDeviceSourceConfiguration().apply {
    setSize(BroadcastConfiguration.Vec2(320f, 180f))
    setPosition(BroadcastConfiguration.Vec2(20f, mixedImageConfig.size.y - size.y - 20))
    setZIndex(2)
}
val camera = deviceDiscovery.listLocalDevices().firstNotNullOf { it as? CameraSource }
val cameraSource = MixedImageDeviceSource(cameraConfig, camera)
mixedImageDevice.addSource(cameraSource)

// Top Right
val streamConfig = MixedImageDeviceSourceConfiguration().apply {
    setSize(BroadcastConfiguration.Vec2(640f, 320f))
    setPosition(BroadcastConfiguration.Vec2(mixedImageConfig.size.x - size.x - 20, 20f))
    setZIndex(1)
}
val streamDevice = deviceDiscovery.createImageInputSource(streamConfig.size)
val streamSource = MixedImageDeviceSource(streamConfig, streamDevice)
mixedImageDevice.addSource(streamSource)

// Bottom Right
val logoConfig = MixedImageDeviceSourceConfiguration().apply {
    setSize(BroadcastConfiguration.Vec2(320f, 180f))
    setPosition(BroadcastConfiguration.Vec2(mixedImageConfig.size.x - size.x - 20, mixedImageConfig.size.y - size.y - 20))
    setZIndex(1)
}
val logoDevice = deviceDiscovery.createImageInputSource(logoConfig.size)
val logoSource = MixedImageDeviceSource(logoConfig, logoDevice)
mixedImageDevice.addSource(logoSource)
```

## Eliminación de fuentes
<a name="broadcast-mixed-devices-removing-sources"></a>

Para eliminar una fuente, llama a `MixedDevice.remove` con el objeto `Source` que deseas eliminar.

## Animaciones con transiciones
<a name="broadcast-mixed-devices-animations-transitions"></a>

El método de transición reemplaza la configuración de una fuente por una nueva configuración. Este reemplazo se puede animar a lo largo del tiempo estableciendo una duración superior a 0 (en segundos). 

### ¿Qué propiedades se pueden animar?
<a name="broadcast-mixed-devices-animations-properties"></a>

No se pueden animar todas las propiedades de la estructura de la ranura. Las propiedades basadas en tipos Float se pueden animar; otras propiedades se hacen efectivas al principio o al final de la animación.


| Nombre | ¿Se puede animar? | Punto de impacto | 
| --- | --- | --- | 
| `Audio.gain` | Sí | Interpolado | 
| `Image.alpha` | Sí | Interpolado | 
| `Image.aspect` | No | Fin | 
| `Image.fillColor` | Sí | Interpolado | 
| `Image.position` | Sí | Interpolado | 
| `Image.size` | Sí | Interpolado | 
| `Image.zIndex` Nota: El `zIndex` mueve planos 2D a través del espacio 3D, de modo que la transición se produce cuando los dos planos se cruzan en algún punto en el medio de la animación. Esto podría calcularse, pero depende de los valores iniciales y finales de `zIndex`. Para lograr una transición más fluida, combínela con `alpha`.  | Sí | Desconocido | 

### Ejemplos sencillos
<a name="broadcast-mixed-devices-animations-examples"></a>

A continuación se muestran ejemplos de una toma de cámara a pantalla completa utilizando la configuración definida anteriormente en [Creación y configuración de un dispositivo de imagen mixto](#broadcast-mixed-image-device-creating-configuring). Se anima durante 0,5 segundos.

#### iOS
<a name="broadcast-mixed-devices-animations-examples-ios"></a>

```
// Continuing the example from above, modifying the existing cameraConfig object.
cameraConfig.size = CGSize(width: 1280, height: 720)
cameraConfig.position = CGPoint.zero
cameraSource.transition(to: cameraConfig, duration: 0.5) { completed in
    if completed {
        print("Animation completed")
    } else {
        print("Animation interrupted")
    }
}
```

#### Android
<a name="broadcast-mixed-devices-animations-examples-android"></a>

```
// Continuing the example from above, modifying the existing cameraConfig object.
cameraConfig.setSize(BroadcastConfiguration.Vec2(1280f, 720f))
cameraConfig.setPosition(BroadcastConfiguration.Vec2(0f, 0f))
cameraSource.transitionToConfiguration(cameraConfig, 500) { completed ->
    if (completed) {
        print("Animation completed")
    } else {
        print("Animation interrupted")
    }
}
```

## Duplicar la transmisión
<a name="broadcast-mixed-devices-mirroring"></a>


| Para duplicar un dispositivo de imagen conectado a la transmisión en esta dirección… | Utilice un valor negativo para… | 
| --- | --- | 
| Horizontalmente | El ancho de la ranura | 
| Verticalmente | La altura de la ranura | 
| Tanto horizontal como verticalmente | Tanto el ancho como la altura de la ranura | 

Será necesario ajustar la posición con el mismo valor para colocar la ranura en la posición correcta cuando se duplique.

A continuación se muestran algunos ejemplos de cómo duplicar la transmisión horizontal y verticalmente.

### iOS
<a name="broadcast-mixed-devices-mirroring-ios"></a>

Duplicación horizontal:

```
let cameraSource = IVSMixedImageDeviceSourceConfiguration()
cameraSource.size = CGSize(width: -320, height: 720)
// Add 320 to position x since our width is -320
cameraSource.position = CGPoint(x: 320, y: 0)
```

Duplicación vertical:

```
let cameraSource = IVSMixedImageDeviceSourceConfiguration()
cameraSource.size = CGSize(width: 320, height: -720)
// Add 720 to position y since our height is -720
cameraSource.position = CGPoint(x: 0, y: 720)
```

### Android
<a name="broadcast-mixed-devices-mirroring-android"></a>

Duplicación horizontal:

```
val cameraConfig = MixedImageDeviceSourceConfiguration().apply {
    setSize(BroadcastConfiguration.Vec2(-320f, 180f))
   // Add 320f to position x since our width is -320f
    setPosition(BroadcastConfiguration.Vec2(320f, 0f))
}
```

Duplicación vertical:

```
val cameraConfig = MixedImageDeviceSourceConfiguration().apply {
    setSize(BroadcastConfiguration.Vec2(320f, -180f))
    // Add 180f to position y since our height is -180f
    setPosition(BroadcastConfiguration.Vec2(0f, 180f))
}
```

Nota: Este método de duplicación `setMirrored` es diferente en `ImagePreviewView` (Android) y `IVSImagePreviewView` (iOS). Este método solo afecta a la vista previa local del dispositivo y no afecta a la transmisión.

# SDK de transmisión de IVS: intercambio de tokens \$1 Transmisión en tiempo real
<a name="broadcast-mobile-token-exchange"></a>

El intercambio de tokens le permite mejorar o reducir las capacidades de los tokens participantes y actualizar los atributos de los tokens en el SDK de transmisión móvil, sin necesidad de que los participantes se vuelvan a conectar. Esto es útil en situaciones como el coalojamiento, en las que los participantes pueden comenzar con capacidades solo de suscripción y posteriormente necesitar capacidades de publicación.

Limitaciones:
+ El intercambio de tokens solo funciona con tokens creados en su servidor mediante un [par de claves](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/getting-started-distribute-tokens.html#getting-started-distribute-tokens-self-signed). No funciona con los tokens creados a través de la API [CreateParticipantToken](https://docs.aws.amazon.com/ivs/latest/RealTimeAPIReference/API_CreateParticipantToken.html). 
+ Si utiliza el intercambio de tokens para cambiar los atributos que rigen las maquetaciones de composición del servidor (como featuredParticipantAttribute y participantOrderAttribute), la maquetación de una composición activa no se actualizará hasta que el participante vuelva a conectarse. 

## Intercambio de tokens
<a name="broadcast-mobile-token-exchange-exchanging-tokens"></a>

El intercambio de tokens es sencillo: llame a la API `exchangeToken` del objeto `Stage` / `IVSStage` y proporciona el nuevo token. Si las `capabilities` del nuevo token son diferentes a las del token anterior, las capacidades del nuevo token se evalúan inmediatamente. Por ejemplo, si el token anterior no tenía la capacidad `publish` y el nuevo sí, se invocan las funciones de la estrategia de escenario para la publicación, lo que permite a la aplicación anfitriona decidir si quiere publicar de inmediato con la nueva capacidad o esperar. Lo mismo ocurre con las capacidades eliminadas: si el token anterior tenía la capacidad `publish` y el nuevo no, el participante anula de inmediato la publicación sin invocar las funciones de estrategia de la fase de publicación.

Al intercambiar un token, el token anterior y el nuevo deben tener los mismos valores para los siguientes campos de carga útil: 
+ `topic`
+ `resource`
+ `jti`
+ `whip_url`
+ `events_url`

Estos campos son inmutables. Si se intercambia un token que modifica un campo inmutable, el SDK rechaza de manera inmediata el intercambio.

Los campos restantes se pueden cambiar, incluidos:
+ `attributes`
+ `capabilities`
+ `user`
+ `_id`
+ `iat`
+ `exp`

### iOS
<a name="broadcast-mobile-token-exchange-exchanging-tokens-ios"></a>



```
let stage = try IVSStage(token: originalToken, strategy: self)
stage.join()
stage.exchangeToken(newToken)
```

### Android
<a name="broadcast-mobile-token-exchange-exchanging-tokens-android"></a>



```
val stage = Stage(context, originalToken, strategy)
stage.join()
stage.exchangeToken(newToken)
```

## Recepción de actualizaciones
<a name="broadcast-mobile-token-exchange-receiving-updates"></a>

Una función en `StageRenderer` / `IVSStageRenderer` recibe actualizaciones sobre los participantes remotos ya publicados que intercambian sus tokens para actualizar su `userId` o `attributes`. Los participantes remotos que aún no publiquen expondrán sus `userId` y `attributes` actualizados a través de las funciones de renderizado `onParticipantJoined` / `participantDidJoin` existentes si finalmente publican

### iOS
<a name="broadcast-mobile-token-exchange-receiving-updates-ios"></a>



```
class MyStageRenderer: NSObject, IVSStageRenderer {
    func stage(_ stage: IVSStage, participantMetadataDidUpdate participant: IVSParticipantInfo) {
        // participant will be a new IVSParticipantInfo instance with updated properties.
    }
}
```

### Android
<a name="broadcast-mobile-token-exchange-receiving-updates-android"></a>



```
private val stageRenderer = object : StageRenderer {
    override fun onParticipantMetadataUpdated(stage: Stage, participantInfo: ParticipantInfo) {
        // participantInfo will be a new ParticipantInfo instance with updated properties.
    }
}
```

## Visibilidad de actualizaciones
<a name="broadcast-mobile-token-exchange-visibility"></a>

Cuando un participante intercambia un token para actualizar su `userId` o `attributes`, la visibilidad de estos cambios depende de su estado de publicación actual: 
+ **Si el participante *no* publica:** la actualización se procesa de forma silenciosa. Si finalmente se publican, todos los SDK recibirán la información actualizada de `userId` y `attributes` como parte del evento de publicación inicial.
+ **Si el participante ya *está* publicando:** la actualización se difunde de forma inmediata. Sin embargo, solo los SDK para móviles de la versión 1.37.0 en adelante reciben la notificación. Los participantes que utilizan el SDK web, los SDK móviles más antiguos y la composición del servidor no ven el cambio hasta que el participante anula la publicación y la vuelve a publicar.

Esta tabla aclara la matriz de soporte:


| Estado del participante | Observer: SDK para móviles, versión 1.37.0 en adelante | Observer: versiones anteriores del SDK para móviles, SDK web, composición del servidor | 
| --- | --- | --- | 
| No se publica (luego comienza) | ✅ Visible (al publicarse cuando el participante se haya unido al evento) | ✅ Visible (al publicarse cuando el participante se haya unido al evento) | 
| Ya publica (nunca vuelve a publicar) | ✅ Visible (inmediatamente a través del evento actualizado de los metadatos de los participantes) | ❌ No visible | 
| Ya publica (deja de publicar y vuelve a publicar) | ✅ Visible (inmediatamente a través del evento actualizado de los metadatos de los participantes) | ✅ Visible más adelante (cuando vuelve a publicar a través del evento de incorporación del participante) | 

# SDK de transmisión de IVS: orígenes de imágenes personalizados \$1 Transmisión en tiempo real
<a name="broadcast-custom-image-sources"></a>

Los orígenes de entrada de imágenes personalizados permiten que una aplicación proporcione su propia entrada de imagen al SDK de transmisión, en lugar de limitarse a las cámaras predeterminadas. Un origen de imagen personalizado puede ser tan simple como una marca de agua semitransparente o una escena estática de “vuelvo enseguida”, o puede permitir que la aplicación haga un procesamiento personalizado adicional, como agregar filtros de belleza a la cámara.

Cuando utiliza una fuente de entrada de imagen personalizada para el control personalizado de la cámara (como el uso de bibliotecas de filtros de belleza que requieren acceso a la cámara), el SDK de transmisión ya no es responsable de administrar la cámara. En cambio, la aplicación es responsable de manejar correctamente el ciclo de vida de la cámara. Consulte la documentación oficial de la plataforma sobre cómo su aplicación debe administrar la cámara.

## Android
<a name="custom-image-sources-android"></a>

Después de crear una sesión de `DeviceDiscovery`, cree un origen de entrada de imagen:

```
CustomImageSource imageSource = deviceDiscovery.createImageInputSource(new BroadcastConfiguration.Vec2(1280, 720));
```

Este método devuelve un `CustomImageSource`, que es una fuente de imagen respaldada por una Android [Surface](https://developer.android.com/reference/android/view/Surface) (Superficie) estándar. La subclase `SurfaceSource` se puede cambiar de tamaño y rotar. También puede crear un `ImagePreviewView` para mostrar una vista previa de su contenido.

Para recuperar el subyacente `Surface`:

```
Surface surface = surfaceSource.getInputSurface();
```

Este `Surface` se puede usar como búfer de salida para productores de imágenes como Camera2, OpenGL ES y otras bibliotecas. El caso de uso más simple es dibujar directamente un mapa de bits estático o un color en el lienzo de la superficie. Sin embargo, muchas bibliotecas (como las bibliotecas de filtros de belleza) proporcionan un método que permite que una aplicación especifique un `Surface` externo para la representación. Puede usar dicho método para pasar el `Surface` a la biblioteca de filtros, lo que permite que la biblioteca genere fotogramas procesados ​​para que la sesión de transmisión los transmita.

Este `CustomImageSource` se puede encapsular en una `LocalStageStream`. La `StageStrategy` lo puede devolver para publicarlo en un `Stage`.

## iOS
<a name="custom-image-sources-ios"></a>

Después de crear una sesión de `DeviceDiscovery`, cree un origen de entrada de imagen:

```
let customSource = broadcastSession.createImageSource(withName: "customSourceName")
```

Este método devuelve un `IVSCustomImageSource`, que es una fuente de imagen que permite que la aplicación envíe `CMSampleBuffers` manualmente. Para conocer los formatos de píxeles admitidos, consulte la referencia del SDK de transmisión de iOS; un enlace a la versión más actual se encuentra en las [Notas de la versión de Amazon IVS](release-notes.md) para la última versión del SDK de transmisión.

Las muestras enviadas al origen personalizado se transmitirán en el escenario:

```
customSource.onSampleBuffer(sampleBuffer)
```

Para transmitir video, utilice este método en una devolución de llamada. Por ejemplo, si está utilizando la cámara, cada vez que se recibe un nuevo búfer de muestra de un `AVCaptureSession`, la aplicación puede reenviar el búfer de muestra a la fuente de imagen personalizada. Si lo desea, la aplicación puede aplicar más procesamiento (como un filtro de belleza) antes de enviar la muestra a la fuente de imagen personalizada.

El `IVSCustomImageSource` se puede encapsular en `IVSLocalStageStream`. La `IVSStageStrategy` lo puede devolver para publicarlo en un `Stage`.

# SDK de transmisión de IVS: orígenes de audio personalizados \$1 Transmisión en tiempo real
<a name="broadcast-custom-audio-sources"></a>

**Nota:** Esta guía solo se aplica al SDK de transmisión de Android de transmisión en tiempo real con IVS. La información sobre los SDK para iOS y la web se publicará en el futuro.

Los orígenes de entrada de audio personalizados permiten que una aplicación proporcione su propia entrada de audio al SDK de transmisión, en vez de limitarse al micrófono integrado del dispositivo. Un origen de audio personalizado permite a las aplicaciones transmitir audio procesado con efectos, mezclar varias transmisiones de audio o integrarse con bibliotecas de procesamiento de audio de terceros.

Cuando utiliza un origen de entrada de imagen, el SDK de transmisión deja de ser responsable de la administración directa del micrófono. En su lugar, su aplicación es responsable de capturar, procesar y enviar los datos de audio al origen personalizado.

El flujo de trabajo del origen de audio personalizado sigue estos pasos:

1. Entrada de audio: cree un origen de audio personalizado con un formato de audio específico (frecuencia de muestreo, canales, formato). 

1. Su procesamiento: capture o genere datos de audio a partir de su canalización de procesamiento de audio.

1. Origen de audio personalizado: envíe los búferes de audio al origen personalizado mediante `appendBuffer()`.

1. Escenario: encapsúlelo en `LocalStageStream` y publíquelo en el escenario a través de su `StageStrategy`. 

1. Participantes: los participantes del escenario reciben el audio procesado en tiempo real.

## Android
<a name="custom-audio-sources-android"></a>

### Creación de un origen de audio personalizado
<a name="custom-audio-sources-android-creating-a-custom-audio-source"></a>

Después de crear una sesión de `DeviceDiscovery`, cree un origen de entrada de audio:

```
DeviceDiscovery deviceDiscovery = new DeviceDiscovery(context); 
 
// Create custom audio source with specific format 
CustomAudioSource customAudioSource = deviceDiscovery.createAudioInputSource( 
   2,  // Number of channels (1 = mono, 2 = stereo) 
   BroadcastConfiguration.AudioSampleRate.RATE_48000,  // Sample rate 
   AudioDevice.Format.INT16  // Audio format (16-bit PCM) 
);
```

Este método devuelve un `CustomAudioSource`, que acepta datos de audio PCM sin procesar. El origen de audio personalizado debe configurarse con el mismo formato de audio que produce la canalización de procesamiento de audio.

#### Formatos admitidos de audio
<a name="custom-audio-sources-android-submitting-audio-data-supportedi-audio-formats"></a>


| Parámetro | Opciones | Descripción | 
| --- | --- | --- | 
| Canales | 1 (mono), 2 (estéreo) | Número de canales de audio. | 
| Velocidad de muestreo | RATE\$116000, RATE\$144100, RATE\$148000 | Frecuencia de muestreo de audio en Hz. Se recomienda 48 kHz para una alta calidad. | 
| Formato | INT16, FLOAT32 | Formato de muestra de audio. INT16 es un PCM de punto fijo de 16 bits, y FLOAT32 es un PCM de punto flotante de 32 bits. Están disponibles los formatos intercalado y plano. | 

### Envío de datos de audio
<a name="custom-audio-sources-android-submitting-audio-data"></a>

Para enviar datos de audio al origen personalizado, utilice el método `appendBuffer()`:

```
// Prepare audio data in a ByteBuffer 
ByteBuffer audioBuffer = ByteBuffer.allocateDirect(bufferSize); 
audioBuffer.put(pcmAudioData);  // Your processed audio data 
 
// Calculate the number of bytes 
long byteCount = pcmAudioData.length; 
 
// Submit audio to the custom source 
// presentationTimeUs should be generated by and come from your audio source
int samplesProcessed = customAudioSource.appendBuffer( 
   audioBuffer, 
   byteCount, 
   presentationTimeUs 
); 
 
if (samplesProcessed > 0) { 
   Log.d(TAG, "Successfully submitted " + samplesProcessed + " samples"); 
} else { 
   Log.w(TAG, "Failed to submit audio samples"); 
} 
 
// Clear buffer for reuse 
audioBuffer.clear();
```

**Consideraciones importantes:**
+ Los datos de audio deben estar en el formato especificado al crear el origen personalizado.
+ Las marcas de tiempo deben aumentar de forma monótona y ser proporcionadas por su fuente de audio para garantizar una reproducción de audio fluida.
+ Envíe el audio con regularidad para evitar interrupciones en la transmisión.
+ El método devuelve el número de muestras procesadas (0 indica un error). 

### Publicación en un escenario
<a name="custom-audio-sources-android-publishing-to-a-stage"></a>

Encapsule `CustomAudioSource` en un `AudioLocalStageStream` y devuélvalo desde su `StageStrategy`:

```
// Create the audio stream from custom source 
AudioLocalStageStream audioStream = new AudioLocalStageStream(customAudioSource); 
 
// Define your stage strategy 
Strategy stageStrategy = new Strategy() { 
   @NonNull 
   @Override 
   public List<LocalStageStream> stageStreamsToPublishForParticipant( 
         @NonNull Stage stage, 
         @NonNull ParticipantInfo participantInfo) { 
      List<LocalStageStream> streams = new ArrayList<>(); 
      streams.add(audioStream);  // Publish custom audio 
      return streams; 
   } 
 
   @Override 
   public boolean shouldPublishFromParticipant( 
         @NonNull Stage stage, 
         @NonNull ParticipantInfo participantInfo) { 
      return true;  // Control when to publish 
   } 
 
   @Override 
   public Stage.SubscribeType shouldSubscribeToParticipant( 
         @NonNull Stage stage, 
         @NonNull ParticipantInfo participantInfo) { 
      return Stage.SubscribeType.AUDIO_VIDEO; 
   } 
}; 
 
// Create and join the stage 
Stage stage = new Stage(context, stageToken, stageStrategy);
```

### Ejemplo completo: integración de procesamiento de audio
<a name="custom-audio-sources-android-complete-example"></a>

Este es un ejemplo completo que muestra la integración con un SDK de procesamiento de audio:

```
public class AudioStreamingActivity extends AppCompatActivity { 
   private DeviceDiscovery deviceDiscovery; 
   private CustomAudioSource customAudioSource; 
   private AudioLocalStageStream audioStream; 
   private Stage stage; 
 
   @Override 
   protected void onCreate(Bundle savedInstanceState) { 
      super.onCreate(savedInstanceState); 
 
      // Configure audio manager 
      StageAudioManager.getInstance(this) 
         .setPreset(StageAudioManager.UseCasePreset.VIDEO_CHAT); 
 
      // Initialize IVS components 
      initializeIVSStage(); 
 
      // Initialize your audio processing SDK 
      initializeAudioProcessing(); 
   } 
 
   private void initializeIVSStage() { 
      deviceDiscovery = new DeviceDiscovery(this); 
 
      // Create custom audio source (48kHz stereo, 16-bit) 
      customAudioSource = deviceDiscovery.createAudioInputSource( 
         2,  // Stereo 
         BroadcastConfiguration.AudioSampleRate.RATE_48000, 
         AudioDevice.Format.INT16 
      ); 
 
      // Create audio stream 
      audioStream = new AudioLocalStageStream(customAudioSource); 
 
      // Create stage with strategy 
      Strategy strategy = new Strategy() { 
         @NonNull 
         @Override 
         public List<LocalStageStream> stageStreamsToPublishForParticipant( 
               @NonNull Stage stage, 
               @NonNull ParticipantInfo participantInfo) { 
            return Collections.singletonList(audioStream); 
         } 
 
         @Override 
         public boolean shouldPublishFromParticipant( 
               @NonNull Stage stage, 
               @NonNull ParticipantInfo participantInfo) { 
            return true; 
         } 
 
         @Override 
         public Stage.SubscribeType shouldSubscribeToParticipant( 
               @NonNull Stage stage, 
               @NonNull ParticipantInfo participantInfo) { 
            return Stage.SubscribeType.AUDIO_VIDEO; 
         } 
      }; 
 
      stage = new Stage(this, getStageToken(), strategy); 
   } 
 
   private void initializeAudioProcessing() { 
      // Initialize your audio processing SDK 
      // Set up callback to receive processed audio 
      yourAudioSDK.setAudioCallback(new AudioCallback() { 
         @Override 
         public void onProcessedAudio(byte[] audioData, int sampleRate, 
                                     int channels, long timestamp) { 
            // Submit processed audio to IVS Stage 
            submitAudioToStage(audioData, timestamp); 
         } 
      }); 
   } 
 
   // The timestamp is required to come from your audio source and you  
   // should not be generating one on your own, unless your audio source 
   // does not provide one. If that is the case, create your own epoch  
   // timestamp and manually calculate the duration between each sample  
   // using the number of frames and frame size. 

   private void submitAudioToStage(byte[] audioData, long timestamp) { 
      try { 
         // Allocate direct buffer 
         ByteBuffer buffer = ByteBuffer.allocateDirect(audioData.length); 
         buffer.put(audioData); 
 
         // Submit to custom audio source 
         int samplesProcessed = customAudioSource.appendBuffer( 
            buffer, 
            audioData.length, 
            timestamp > 0 ? timestamp : System.nanoTime() / 1000 
         ); 
 
         if (samplesProcessed <= 0) { 
            Log.w(TAG, "Failed to submit audio samples"); 
         } 
 
         buffer.clear(); 
      } catch (Exception e) { 
         Log.e(TAG, "Error submitting audio: " + e.getMessage(), e); 
      } 
   } 
 
   @Override 
   protected void onDestroy() { 
      super.onDestroy(); 
      if (stage != null) { 
          stage.release(); 
      } 
   } 
}
```

### Prácticas recomendadas
<a name="custom-audio-sources-android-best-practices"></a>

#### Consistencia de formato de audio
<a name="custom-audio-sources-android-best-practices-audio-format-consistency"></a>

Asegúrese de que el formato de audio que envíe coincida con el formato especificado al crear el origen personalizado:

```
// If you create with 48kHz stereo INT16 
customAudioSource = deviceDiscovery.createAudioInputSource( 
   2, RATE_48000, INT16 
); 
 
// Your audio data must be: 
// - 2 channels (stereo) 
// - 48000 Hz sample rate 
// - 16-bit interleaved PCM format
```

#### Administración de búfer
<a name="custom-audio-sources-android-best-practices-buffer-managemetn"></a>

Use `ByteBuffers` directamente y reutilícelos para minimizar la recopilación de elementos no utilizados: 

```
// Allocate once 
private ByteBuffer audioBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE); 
 
// Reuse in callback 
public void onAudioData(byte[] data) { 
   audioBuffer.clear(); 
   audioBuffer.put(data); 
   customAudioSource.appendBuffer(audioBuffer, data.length, getTimestamp()); 
   audioBuffer.clear(); 
}
```

#### Sincronización y temporización
<a name="custom-audio-sources-android-best-practices-timing-and-synchronization"></a>

Debe utilizar las marcas de tiempo proporcionadas por el origen de audio para que la reproducción del audio sea fluida. Si el origen de audio no proporciona su propia marca de tiempo, cree su propia marca de tiempo de época y calcule de forma manual la duración entre cada muestra mediante el número de fotogramas y el tamaño del fotograma. 

```
// "audioFrameTimestamp" should be generated by your audio source
// Consult your audio source’s documentation for information on how to get this 
long timestamp = audioFrameTimestamp;
```

#### Gestión de errores
<a name="custom-audio-sources-android-best-practices-error-handling"></a>

Compruebe siempre el valor de retorno de `appendBuffer()`: 

```
int samplesProcessed = customAudioSource.appendBuffer(buffer, count, timestamp); 
 
if (samplesProcessed <= 0) { 
   Log.w(TAG, "Audio submission failed - buffer may be full or format mismatch"); 
   // Handle error: check format, reduce submission rate, etc. 
}
```

# SDK de transmisión de IVS: filtros de cámara externos \$1 Transmisión en tiempo real
<a name="broadcast-3p-camera-filters"></a>

Esta guía supone que ya está familiarizado con las fuentes de [imágenes personalizadas](broadcast-custom-image-sources.md) y que ha integrado el [SDK de transmisión en tiempo real de IVS](broadcast.md) en su aplicación.

Los filtros de cámara permiten a los creadores de transmisiones en directo aumentar o modificar su aspecto facial o de fondo. Esto puede aumentar la participación de los espectadores, atraerlos y mejorar la experiencia de transmisión en directo.

# Integración de filtros de cámara externos
<a name="broadcast-3p-camera-filters-integrating"></a>

Puede integrar los SDK de filtros de cámara externos con el SDK de transmisión de IVS introduciendo la salida del SDK de filtros en una [fuente de entrada de imágenes personalizada](broadcast-custom-image-sources.md). Una fuente de entrada de imágenes personalizada permite que una aplicación proporcione su propia entrada de imagen al SDK de transmisión. El SDK de un proveedor de filtros externos puede gestionar el ciclo de vida de la cámara para procesar las imágenes de la cámara, aplicar un efecto de filtro y emitirlas en un formato que se pueda pasar a una fuente de imágenes personalizada.

![\[Integración de los SDK de filtros de cámara externos con el SDK de transmisión de IVS introduciendo la salida del SDK de filtros en una fuente de entrada de imágenes personalizada.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/3P_Camera_Filters_Integrating.png)


Consulte la documentación de su proveedor de filtros externos para conocer los métodos integrados para convertir un fotograma de cámara, con el efecto de filtro, aplicado a un formato que se pueda pasar a una [fuente de entrada de imágenes personalizada](broadcast-custom-image-sources.md). El proceso varía según la versión del SDK de transmisión de IVS que se utilice:
+ **Web**: el proveedor de filtros debe poder renderizar su salida en un elemento de lienzo. Se puede usar el método [CaptureStream](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/captureStream) para devolver un MediaStream del contenido del lienzo. El MediaStream se puede convertir en una instancia de [LocalStageStream](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/classes/LocalStageStream) y publicarse en un escenario.
+ **Android**: el SDK del proveedor de filtros puede renderizar un marco en un dispositivo Android `Surface` proporcionado por el SDK de transmisión de IVS o convertir el marco en un mapa de bits. Si utiliza un mapa de bits, puede renderizarlo en el `Surface` subyacente proporcionado por la fuente de imagen personalizada, desbloqueándolo y escribiéndolo en un lienzo.
+ **iOS**: el SDK de un proveedor de filtros externo debe proporcionar un marco de cámara con un efecto de filtro aplicado como `CMSampleBuffer`. Consulte la documentación del SDK de su proveedor de filtros externo para obtener información sobre cómo obtener un `CMSampleBuffer` como resultado final después de procesar una imagen de cámara.

# Uso de BytePlus con el SDK de transmisión de IVS
<a name="broadcast-3p-camera-filters-integrating-byteplus"></a>

Este documento explica cómo utilizar el SDK de BytePlus Effects con el SDK de transmisión de IVS.

## Android
<a name="integrating-byteplus-android"></a>

### Instalación y configuración del SDK de BytePlus Effects
<a name="integrating-byteplus-android-install-effects-sdk"></a>

Consulte la [Guía de acceso de Android](https://docs.byteplus.com/en/effects/docs/android-v4101-access-guide) para BytePlus para obtener detalles sobre cómo instalar, inicializar y configurar el SDK de BytePlus Effects.

### Configuración de la fuente de imagen personalizada
<a name="integrating-byteplus-android-setup-image-source"></a>

Tras inicializar el SDK, alimente los fotogramas de cámara procesados con un efecto de filtro aplicado a una fuente de entrada de imágenes personalizada. Para ello, cree una instancia de un objeto `DeviceDiscovery` y cree una fuente de imagen personalizada. Observe que cuando utiliza una fuente de entrada de imagen personalizada para el control personalizado de la cámara, el SDK de transmisión ya no es responsable de administrar la cámara. En cambio, la aplicación es responsable de manejar correctamente el ciclo de vida de la cámara.

#### Java
<a name="integrating-byteplus-android-setup-image-source-code"></a>

```
var deviceDiscovery = DeviceDiscovery(applicationContext)
var customSource = deviceDiscovery.createImageInputSource( BroadcastConfiguration.Vec2(
720F, 1280F
))
var surface: Surface = customSource.inputSurface
var filterStream = ImageLocalStageStream(customSource)
```

### Convierta la salida en un mapa de bits y la transmisión en una fuente de entrada de imágenes personalizada
<a name="integrating-byteplus-android-convert-to-bitmap"></a>

Para permitir que los fotogramas de cámara con un efecto de filtro aplicado desde el SDK de BytePlusEffect se reenvíen directamente al SDK de transmisión de IVS, convierta la salida de una textura del SDK de BytePlus Effects en un mapa de bits. Cuando se procesa una imagen, el SDK invoca al método `onDrawFrame()`. El método `onDrawFrame()` es un método público de la interfaz [GLSurfaceView.Renderer](https://developer.android.com/reference/android/opengl/GLSurfaceView.Renderer) de Android. En la aplicación de ejemplo para Android proporcionada por BytePlus, este método se utiliza en todos los fotogramas de la cámara y genera una textura. Al mismo tiempo, puede complementar el método `onDrawFrame()` con la lógica para convertir esta textura en un mapa de bits y enviarla a una fuente de entrada de imágenes personalizada. Como se muestra en el siguiente ejemplo de código, utilice el método `transferTextureToBitmap` proporcionado por el SDK de BytePlus para realizar esta conversión. Este método lo proporciona la biblioteca [com.bytedance.labcv.core.util.ImageUtil](https://docs.byteplus.com/en/effects/docs/android-v4101-access-guide#Appendix:%20convert%20input%20texture%20to%202D%20texture%20with%20upright%20face) del SDK de BytePlus Effects, como se muestra en el siguiente ejemplo de código. Luego, puede renderizar el `Surface` subyacente para Android de `CustomImageSource` al escribir el mapa de bits resultante en el lienzo de Surface. Muchas invocaciones sucesivas de `onDrawFrame()` dan como resultado una secuencia de mapas de bits y, cuando se combinan, crean una secuencia de video.

#### Java
<a name="integrating-byteplus-android-convert-to-bitmap-code"></a>

```
import com.bytedance.labcv.core.util.ImageUtil;
...
protected ImageUtil imageUtility;
...


@Override
public void onDrawFrame(GL10 gl10) {
  ...	
  // Convert BytePlus output to a Bitmap
  Bitmap outputBt = imageUtility.transferTextureToBitmap(output.getTexture(),ByteEffect     
  Constants.TextureFormat.Texture2D,output.getWidth(), output.getHeight());

  canvas = surface.lockCanvas(null);
  canvas.drawBitmap(outputBt, 0f, 0f, null);
  surface.unlockCanvasAndPost(canvas);
```

# Uso de DeepAR con el SDK de transmisión de IVS
<a name="broadcast-3p-camera-filters-integrating-deepar"></a>

Este documento explica cómo utilizar el SDK de DeepAR con el SDK de transmisión de IVS.

## Android
<a name="integrating-deepar-android"></a>

Consulte [Android Integration Guide de DeepAR](https://docs.deepar.ai/deepar-sdk/integrations/video-calling/amazon-ivs/android/) para obtener detalles sobre cómo integrar el SDK de DeepAR con el SDK de retransmisión de Android IVS.

## iOS
<a name="integrating-deepar-ios"></a>

Consulte la [Guía de integración de iOS de DeepAR](https://docs.deepar.ai/deepar-sdk/integrations/video-calling/amazon-ivs/ios/) para obtener más información sobre cómo integrar el SDK de DeepAR con el SDK de transmisión IVS para iOS.

# Uso de Snap con el SDK de transmisión de IVS
<a name="broadcast-3p-camera-filters-integrating-snap"></a>

Este documento explica cómo utilizar el SDK de Camera Kit de Snap con el SDK de transmisión de IVS.

## Web
<a name="integrating-snap-web"></a>

En esta sección se presupone que ya está familiarizado con la [publicación de videos y la suscripción a ellos mediante el SDK de transmisión web](getting-started-pub-sub-web.md).

Para integrar el SDK de Camera Kit de Snap con el SDK de transmisión Web en tiempo real de IVS, debe:

1. Instale el SDK y el Webpack de Camera Kit. (Nuestro ejemplo usa Webpack como paquete, pero puede usar cualquier paquete de su elección).

1. Cree `index.html`.

1. Agregue elementos de configuración.

1. Cree `index.css`.

1. Muestre y configure los participantes.

1. Muestre las cámaras y los micrófonos conectados.

1. Cree una sesión de Camera Kit.

1. Busque lentes y rellene el selector de lentes.

1. Renderice el resultado de una sesión de Camera Kit en un lienzo.

1. Cree una función para rellenar el menú desplegable de lentes.

1. Proporcione a Camera Kit una fuente multimedia para renderizar y publicar un`LocalStageStream`.

1. Cree `package.json`.

1. Cree un archivo de configuración de Webpack.

1. Configure un servidor HTTPS y realice una prueba.

A continuación, se describe cada uno de estos pasos.

### Instalación del SDK y del Webpack de Camera Kit
<a name="integrating-snap-web-install-camera-kit"></a>

En este ejemplo, utilizamos Webpack como nuestro empaquetador, pero puede usar cualquier otro.

```
npm i @snap/camera-kit webpack webpack-cli
```

### Creación del archivo index.html
<a name="integrating-snap-web-create-index"></a>

A continuación, cree la plantilla HTML e importe el SDK de transmisión web como una etiqueta script. En el código siguiente, asegúrese de reemplazar `<SDK version>` por la versión del SDK de transmisión que esté utilizando.

#### HTML
<a name="integrating-snap-web-create-index-code"></a>

```
<!--
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */
-->
<!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" />

  <title>Amazon IVS Real-Time Streaming Web Sample (HTML and JavaScript)</title>

  <!-- Fonts and Styling -->
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css" />
  <link rel="stylesheet" href="./index.css" />

  <!-- Stages in Broadcast SDK -->
  <script src="https://web-broadcast.live-video.net/<SDK version>/amazon-ivs-web-broadcast.js"></script>
</head>

<body>
  <!-- Introduction -->
  <header>
    <h1>Amazon IVS Real-Time Streaming Web Sample (HTML and JavaScript)</h1>

    <p>This sample is used to demonstrate basic HTML / JS usage. <b><a href="https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/multiple-hosts.html">Use the AWS CLI</a></b> to create a <b>Stage</b> and a corresponding <b>ParticipantToken</b>. Multiple participants can load this page and put in their own tokens. You can <b><a href="https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#glossary" target="_blank">read more about stages in our public docs.</a></b></p>
  </header>
  <hr />
  
  <!-- Setup Controls -->
 
  <!-- Display Local Participants -->
  
  <!-- Lens Selector -->

  <!-- Display Remote Participants -->

  <!-- Load All Desired Scripts -->
```

### Agregue elementos de configuración
<a name="integrating-snap-web-add-setup-elements"></a>

Cree el código HTML para seleccionar una cámara, un micrófono y un lente y para especificar un token de participante:

#### HTML
<a name="integrating-snap-web-setup-controls-code"></a>

```
<!-- Setup Controls -->
  <div class="row">
    <div class="column">
      <label for="video-devices">Select Camera</label>
      <select disabled id="video-devices">
        <option selected disabled>Choose Option</option>
      </select>
    </div>
    <div class="column">
      <label for="audio-devices">Select Microphone</label>
      <select disabled id="audio-devices">
        <option selected disabled>Choose Option</option>
      </select>
    </div>
    <div class="column">
      <label for="token">Participant Token</label>
      <input type="text" id="token" name="token" />
    </div>
    <div class="column" style="display: flex; margin-top: 1.5rem">
      <button class="button" style="margin: auto; width: 100%" id="join-button">Join Stage</button>
    </div>
    <div class="column" style="display: flex; margin-top: 1.5rem">
      <button class="button" style="margin: auto; width: 100%" id="leave-button">Leave Stage</button>
    </div>
  </div>
```

Añada un código HTML adicional debajo para mostrar las imágenes de las cámaras de los participantes locales y remotos:

#### HTML
<a name="integrating-snap-web-local-remote-participants-code"></a>

```
 <!-- Local Participant -->
<div class="row local-container">
    <canvas id="canvas"></canvas>

    <div class="column" id="local-media"></div>
    <div class="static-controls hidden" id="local-controls">
      <button class="button" id="mic-control">Mute Mic</button>
      <button class="button" id="camera-control">Mute Camera</button>
    </div>
  </div>

  
  <hr style="margin-top: 5rem"/>
  
  <!-- Remote Participants -->
  <div class="row">
    <div id="remote-media"></div>
  </div>
```

Cargue lógica adicional, incluidos los métodos de ayuda para configurar la cámara y el archivo JavaScript incluido. (Más adelante en esta sección, creará estos archivos JavaScript y los agrupará en un solo archivo para poder importar Camera Kit como un módulo. El archivo JavaScript agrupado contendrá la lógica para configurar Camera Kit, aplicar un lente y publicar la imagen de la cámara con un lente aplicado a un escenario). Agregue etiquetas de cierre a los elementos `body` y `html` para completar la creación del `index.html`.

#### HTML
<a name="integrating-snap-web-load-all-scripts-code"></a>

```
<!-- Load all Desired Scripts -->
  <script src="./helpers.js"></script>
  <script src="./media-devices.js"></script>
  <!-- <script type="module" src="./stages-simple.js"></script> -->
  <script src="./dist/bundle.js"></script>
</body>
</html>
```

### Creación del archivo index.css
<a name="integrating-snap-web-create-index-css"></a>

Cree un archivo de origen CSS para diseñar la página. No nos detendremos en este código, sino que nos centraremos en la lógica para administrar un escenario e integrarlo en el SDK de Camera Kit de Snap.

#### CSS
<a name="integrating-snap-web-create-index-css-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

html,
body {
  margin: 2rem;
  box-sizing: border-box;
  height: 100vh;
  max-height: 100vh;
  display: flex;
  flex-direction: column;
}

hr {
  margin: 1rem 0;
}

table {
  display: table;
}

canvas {
  margin-bottom: 1rem;
  background: green;
}

video {
  margin-bottom: 1rem;
  background: black;
  max-width: 100%;
  max-height: 150px;
}

.log {
  flex: none;
  height: 300px;
}

.content {
  flex: 1 0 auto;
}

.button {
  display: block;
  margin: 0 auto;
}

.local-container {
  position: relative;
}

.static-controls {
  position: absolute;
  margin-left: auto;
  margin-right: auto;
  left: 0;
  right: 0;
  bottom: -4rem;
  text-align: center;
}

.static-controls button {
  display: inline-block;
}

.hidden {
  display: none;
}

.participant-container {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  margin: 1rem;
}

video {
  border: 0.5rem solid #555;
  border-radius: 0.5rem;
}
.placeholder {
  background-color: #333333;
  display: flex;
  text-align: center;
  margin-bottom: 1rem;
}
.placeholder span {
  margin: auto;
  color: white;
}
#local-media {
  display: inline-block;
  width: 100vw;
}

#local-media video {
  max-height: 300px;
}

#remote-media {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: row;
  width: 100%;
}

#lens-selector {
  width: 100%;
  margin-bottom: 1rem;
}
```

### Visualización y configuración de los participantes
<a name="integrating-snap-web-setup-participants"></a>

A continuación, cree `helpers.js`, que contiene métodos auxiliares que utilizará para mostrar y configurar los participantes:

#### JavaScript
<a name="integrating-snap-web-setup-participants-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

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

### Visualización de las cámaras y los micrófonos conectados
<a name="integrating-snap-web-display-cameras-microphones"></a>

A continuación, cree `media-devices.js`, que contiene métodos auxiliares para mostrar las cámaras y los micrófonos conectados a su dispositivo:

#### JavaScript
<a name="integrating-snap-web-display-cameras-microphones-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

/**
 * Returns an initial list of devices populated on the page selects
 */
async function initializeDeviceSelect() {
  const videoSelectEl = document.getElementById('video-devices');
  videoSelectEl.disabled = false;

  const { videoDevices, audioDevices } = await getDevices();
  videoDevices.forEach((device, index) => {
    videoSelectEl.options[index] = new Option(device.label, device.deviceId);
  });

  const audioSelectEl = document.getElementById('audio-devices');

  audioSelectEl.disabled = false;
  audioDevices.forEach((device, index) => {
    audioSelectEl.options[index] = new Option(device.label, device.deviceId);
  });
}

/**
 * Returns all devices available on the current device
 */
async function getDevices() {
  // Prevents issues on Safari/FF so devices are not blank
  await navigator.mediaDevices.getUserMedia({ video: true, audio: true });

  const devices = await navigator.mediaDevices.enumerateDevices();
  // Get all video devices
  const videoDevices = devices.filter((d) => d.kind === 'videoinput');
  if (!videoDevices.length) {
    console.error('No video devices found.');
  }

  // Get all audio devices
  const audioDevices = devices.filter((d) => d.kind === 'audioinput');
  if (!audioDevices.length) {
    console.error('No audio devices found.');
  }

  return { videoDevices, audioDevices };
}

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

async function getMic(deviceId) {
  return navigator.mediaDevices.getUserMedia({
    video: false,
    audio: {
      deviceId: deviceId ? { exact: deviceId } : null,
    },
  });
}
```

### Creación de una sesión de Camera Kit
<a name="integrating-snap-web-camera-kit-session"></a>

Cree `stages.js`, que contiene la lógica para aplicar una lente a la transmisión de la cámara y publicar la transmisión en un escenario. Recomendamos copiar y pegar el siguiente bloque de código en `stages.js`. A continuación, puede revisar el código parte por parte para entender lo que sucede en las siguientes secciones.

#### JavaScript
<a name="integrating-snap-web-camera-kit-session-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

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

import {
  bootstrapCameraKit,
  createMediaStreamSource,
  Transform2D,
} from '@snap/camera-kit';

let cameraButton = document.getElementById('camera-control');
let micButton = document.getElementById('mic-control');
let joinButton = document.getElementById('join-button');
let leaveButton = document.getElementById('leave-button');

let controls = document.getElementById('local-controls');
let videoDevicesList = document.getElementById('video-devices');
let audioDevicesList = document.getElementById('audio-devices');

let lensSelector = document.getElementById('lens-selector');
let session;
let availableLenses = [];

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

const liveRenderTarget = document.getElementById('canvas');

const init = async () => {
  await initializeDeviceSelect();

  const cameraKit = await bootstrapCameraKit({
    apiToken: 'INSERT_YOUR_API_TOKEN_HERE',
  });

  session = await cameraKit.createSession({ liveRenderTarget });
  const { lenses } = await cameraKit.lensRepository.loadLensGroups([
    'INSERT_YOUR_LENS_GROUP_ID_HERE',
  ]);

  availableLenses = lenses;
  populateLensSelector(lenses);

  const snapStream = liveRenderTarget.captureStream();

  lensSelector.addEventListener('change', handleLensChange);
  lensSelector.disabled = true;
  cameraButton.addEventListener('click', () => {
    const isMuted = !cameraStageStream.isMuted;
    cameraStageStream.setMuted(isMuted);
    cameraButton.innerText = isMuted ? 'Show Camera' : 'Hide Camera';
  });

  micButton.addEventListener('click', () => {
    const isMuted = !micStageStream.isMuted;
    micStageStream.setMuted(isMuted);
    micButton.innerText = isMuted ? 'Unmute Mic' : 'Mute Mic';
  });

  joinButton.addEventListener('click', () => {
    joinStage(session, snapStream);
  });

  leaveButton.addEventListener('click', () => {
    leaveStage();
  });
};

async function setCameraKitSource(session, mediaStream) {
  const source = createMediaStreamSource(mediaStream);
  await session.setSource(source);
  source.setTransform(Transform2D.MirrorX);
  session.play();
}

const populateLensSelector = (lenses) => {
  lensSelector.innerHTML = '<option selected disabled>Choose Lens</option>';

  lenses.forEach((lens, index) => {
    const option = document.createElement('option');
    option.value = index;
    option.text = lens.name || `Lens ${index + 1}`;
    lensSelector.appendChild(option);
  });
};

const handleLensChange = (event) => {
  const selectedIndex = parseInt(event.target.value);
  if (session && availableLenses[selectedIndex]) {
    session.applyLens(availableLenses[selectedIndex]);
  }
};

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

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

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

  // Retrieve the User Media currently set on the page
  localCamera = await getCamera(videoDevicesList.value);
  localMic = await getMic(audioDevicesList.value);
  await setCameraKitSource(session, localCamera);

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

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

  stage = new Stage(token, strategy);

  // Other available events:
  // https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#events
  stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {
    connected = state === ConnectionState.CONNECTED;

    if (connected) {
      joining = false;
      controls.classList.remove('hidden');
      lensSelector.disabled = false;
    } else {
      controls.classList.add('hidden');
      lensSelector.disabled = true;
    }
  });

  stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {
    console.log('Participant Joined:', participant);
  });

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

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

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

  joining = false;
  connected = false;

  cameraButton.innerText = 'Hide Camera';
  micButton.innerText = 'Mute Mic';
  controls.classList.add('hidden');
};

init();
```

En la primera parte de este archivo, importamos el SDK de transmisión y el SDK web de Camera Kit e inicializamos las variables que usaremos con cada SDK. Para crear una sesión de Camera Kit, activamos `createSession` después de [impulsar el SDK web de Camera Kit](https://kit.snapchat.com/reference/CameraKit/web/0.7.0/index.html#bootstrapping-the-sdk). Tenga en cuenta que un objeto de elemento de lienzo se pasa a una sesión; esto le indica a Camera Kit que lo renderice en ese lienzo.

#### JavaScript
<a name="integrating-snap-web-camera-kit-session-code-2"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

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

import {
  bootstrapCameraKit,
  createMediaStreamSource,
  Transform2D,
} from '@snap/camera-kit';

let cameraButton = document.getElementById('camera-control');
let micButton = document.getElementById('mic-control');
let joinButton = document.getElementById('join-button');
let leaveButton = document.getElementById('leave-button');

let controls = document.getElementById('local-controls');
let videoDevicesList = document.getElementById('video-devices');
let audioDevicesList = document.getElementById('audio-devices');

let lensSelector = document.getElementById('lens-selector');
let session;
let availableLenses = [];

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

const liveRenderTarget = document.getElementById('canvas');

const init = async () => {
  await initializeDeviceSelect();

  const cameraKit = await bootstrapCameraKit({
    apiToken: 'INSERT_YOUR_API_TOKEN_HERE',
  });

  session = await cameraKit.createSession({ liveRenderTarget });
```

### Búsqueda de lentes y rellenado del selector de lentes
<a name="integrating-snap-web-fetch-apply-lens"></a>

Para buscar sus lentes, remplace el marcador de posición del ID de grupo de lentes con el suyo, el cual puede encontrar en el [portal para desarrolladores de Camera Kit](https://camera-kit.snapchat.com/). Rellene el menú desplegable de selección de lentes con la función `populateLensSelector()` que crearemos más adelante.

#### JavaScript
<a name="integrating-snap-web-fetch-apply-lens-code"></a>

```
session = await cameraKit.createSession({ liveRenderTarget });
  const { lenses } = await cameraKit.lensRepository.loadLensGroups([
    'INSERT_YOUR_LENS_GROUP_ID_HERE',
  ]);

  availableLenses = lenses;
  populateLensSelector(lenses);
```

### Renderización del resultado de una sesión de Camera Kit en un lienzo
<a name="integrating-snap-web-render-output-to-canvas"></a>

Utilice el método [CaptureStream](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/captureStream) para devolver una `MediaStream` del contenido del lienzo. El lienzo contendrá una transmisión de video de la imagen de la cámara con una lente aplicada. Además, añada oyentes de eventos como botones para silenciar la cámara y el micrófono, así como oyentes de eventos para entrar y salir del escenario. En el oyente de eventos para unirse a un escenario, pasamos una sesión de Camera Kit y la `MediaStream` del lienzo para poder publicarla en un escenario.

#### JavaScript
<a name="integrating-snap-web-render-output-to-canvas-code"></a>

```
const snapStream = liveRenderTarget.captureStream();

  lensSelector.addEventListener('change', handleLensChange);
  lensSelector.disabled = true;
  cameraButton.addEventListener('click', () => {
    const isMuted = !cameraStageStream.isMuted;
    cameraStageStream.setMuted(isMuted);
    cameraButton.innerText = isMuted ? 'Show Camera' : 'Hide Camera';
  });

  micButton.addEventListener('click', () => {
    const isMuted = !micStageStream.isMuted;
    micStageStream.setMuted(isMuted);
    micButton.innerText = isMuted ? 'Unmute Mic' : 'Mute Mic';
  });

  joinButton.addEventListener('click', () => {
    joinStage(session, snapStream);
  });

  leaveButton.addEventListener('click', () => {
    leaveStage();
  });
};
```

### Creación de una función para rellenar el menú desplegable de lentes
<a name="integrating-snap-web-populate-lens-dropdown"></a>

Cree la siguiente función para rellenar el selector de **lentes** con los lentes que buscó anteriormente. El selector de **lentes** es un elemento de la interfaz de usuario de la página que permite elegir lentes de una lista y aplicarlos a la imagen de la cámara. Además, cree la función de devolución de llamada `handleLensChange` para aplicar el lente especificado cuando esté seleccionado en el menú desplegable de **lentes**.

#### JavaScript
<a name="integrating-snap-web-populate-lens-dropdown-code"></a>

```
const populateLensSelector = (lenses) => {
  lensSelector.innerHTML = '<option selected disabled>Choose Lens</option>';

  lenses.forEach((lens, index) => {
    const option = document.createElement('option');
    option.value = index;
    option.text = lens.name || `Lens ${index + 1}`;
    lensSelector.appendChild(option);
  });
};

const handleLensChange = (event) => {
  const selectedIndex = parseInt(event.target.value);
  if (session && availableLenses[selectedIndex]) {
    session.applyLens(availableLenses[selectedIndex]);
  }
};
```

### Proporcione a Camera Kit una fuente multimedia para renderizar y publique un LocalStageStream
<a name="integrating-snap-web-publish-localstagestream"></a>

Para publicar una transmisión de video con una lente aplicada, cree una función llamada `setCameraKitSource` para transferir la `MediaStream` capturada anteriormente desde el lienzo. La `MediaStream` desde el lienzo no sirve de nada por el momento porque aún no hemos incorporado nuestra cámara local. Podemos incorporar la señal de nuestra cámara local llamando al método auxiliar `getCamera` y asignándolo a `localCamera`. Luego, podemos pasar la señal de nuestra cámara local (vía`localCamera`) y el objeto de sesión a `setCameraKitSource`. La función `setCameraKitSource` convierte la señal de nuestra cámara local en una [fuente de contenido multimedia para Camera Kit](https://docs.snap.com/camera-kit/integrate-sdk/web/web-configuration#creating-a-camerakitsource) con solo una llamada a `createMediaStreamSource`. La fuente multimedia para `CameraKit` se [transforma](https://docs.snap.com/camera-kit/integrate-sdk/web/web-configuration#2d-transforms) para reflejar la cámara frontal. El efecto de la lente se aplica a la fuente multimedia y se renderiza en el lienzo de salida mediante una llamada`session.play()`.

Ahora que la lente está aplicada a la `MediaStream` capturada desde el lienzo, podemos proceder a publicarla en un escenario. Para ello, creamos un `LocalStageStream` con las pistas de video de `MediaStream`. Luego, se puede pasar una instancia de `LocalStageStream` a `StageStrategy` para publicarla.

#### JavaScript
<a name="integrating-snap-web-publish-localstagestream-code"></a>

```
async function setCameraKitSource(session, mediaStream) {
  const source = createMediaStreamSource(mediaStream);
  await session.setSource(source);
  source.setTransform(Transform2D.MirrorX);
  session.play();
}

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

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

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

  // Retrieve the User Media currently set on the page
  localCamera = await getCamera(videoDevicesList.value);
  localMic = await getMic(audioDevicesList.value);
  await setCameraKitSource(session, localCamera);
  // Create StageStreams for Audio and Video
  // cameraStageStream = new LocalStageStream(localCamera.getVideoTracks()[0]);
  cameraStageStream = new LocalStageStream(snapStream.getVideoTracks()[0]);
  micStageStream = new LocalStageStream(localMic.getAudioTracks()[0]);

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

El código restante que aparece a continuación sirve para crear y gestionar nuestro escenario:

#### JavaScript
<a name="integrating-snap-web-create-manage-stage-code"></a>

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

  // Other available events:
  // https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#events

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

    if (connected) {
      joining = false;
      controls.classList.remove('hidden');
    } else {
      controls.classList.add('hidden');
    }
  });

  stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {
    console.log('Participant Joined:', participant);
  });

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

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

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

  joining = false;
  connected = false;

  cameraButton.innerText = 'Hide Camera';
  micButton.innerText = 'Mute Mic';
  controls.classList.add('hidden');
};

init();
```

### Creación del archivo package.json
<a name="integrating-snap-web-package-json"></a>

Cree el archivo `package.json` y agregue la siguiente configuración de JSON. Este archivo define nuestras dependencias e incluye un comando de script para agrupar nuestro código.

#### Configuración de JSON
<a name="integrating-snap-web-package-json-code"></a>

```
{
  "dependencies": {
    "@snap/camera-kit": "^0.10.0"
  },
  "name": "ivs-stages-with-snap-camerakit",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "webpack": "^5.95.0",
    "webpack-cli": "^5.1.4"
  }
}
```

### Creación de un archivo de configuración de Webpack
<a name="integrating-snap-web-webpack-config"></a>

Cree `webpack.config.js` y agregue el siguiente código. Esto agrupa el código que hemos creado hasta ahora para que podamos usar la declaración de importación para usar Camera Kit.

#### JavaScript
<a name="integrating-snap-web-webpack-config-code"></a>

```
const path = require('path');
module.exports = {
  entry: ['./stage.js'],
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};
```

Por último, ejecute `npm run build` para agrupar su JavaScript como se define en el archivo de configuración de Webpack. Para la prueba, puede servir a HTML y JavaScript desde su computadora local. En este ejemplo, utilizamos el módulo `http.server` de Python. 

### Configuración y prueba de un servidor HTTPS
<a name="integrating-snap-web-https-server-test"></a>

Para probar nuestro código, debemos configurar un servidor HTTPS. Usar un servidor HTTPS en el desarrollo local y las pruebas para integrar la aplicación web en el SDK de Camera Kit de Snap ayudará a evitar problemas relacionados con el CORS (intercambio de recursos entre orígenes).

Abra un terminal y vaya hasta el directorio en que creó todo el código hasta este punto. Use el siguiente comando para generar un certificado SSL/TLS autofirmado y una clave privada:

```
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
```

Esto crea dos archivos: `key.pem` (la clave privada) y `cert.pem` (el certificado autofirmado). Cree un nuevo archivo de Python, denominado `https_server.py`, y agregue el siguiente código:

#### Python
<a name="integrating-snap-web-https-server-test-code"></a>

```
import http.server
import ssl

# Set the directory to serve files from
DIRECTORY = '.'

# Create the HTTPS server
server_address = ('', 4443)
httpd = http.server.HTTPServer(
    server_address, http.server.SimpleHTTPRequestHandler)

# Wrap the socket with SSL/TLS
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('cert.pem', 'key.pem')
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)

print(f'Starting HTTPS server on https://localhost:4443, serving {DIRECTORY}')
httpd.serve_forever()
```

Abra una terminal, navegue hasta el directorio donde creó el archivo `https_server.py` y ejecute el siguiente comando:

```
python3 https_server.py
```

Esto inicia el servidor HTTPS en https://localhost:4443 y entrega archivos del directorio actual. Asegúrese de que los archivos `cert.pem` y `key.pem` se encuentren en el mismo directorio que el archivo `https_server.py`.

Abra el navegador y navegue hasta https://localhost:4443. Como se trata de un certificado SSL/TLS autofirmado, el navegador web no confiará en él, por lo que recibirá una advertencia. Como esto es solo para fines de prueba, puede omitir la advertencia. A continuación, debería ver en pantalla el efecto AR de la lente de Snap que especificó anteriormente aplicado a la transmisión de la cámara.

Tenga en cuenta que esta configuración, que utiliza los módulos integrados `http.server` y `ssl` los módulos de Python, es adecuada para el desarrollo y las pruebas locales, pero no se recomienda para un entorno de producción. Los navegadores web y otros clientes no confían en el certificado SSL/TLS autofirmado que se usa en esta configuración, lo que significa que los usuarios encontrarán advertencias de seguridad al acceder al servidor. Además, aunque en este ejemplo usamos los módulos http.server y ssl integrados en Python, puede optar por utilizar otra solución de servidor HTTPS.

## Android
<a name="integrating-snap-android"></a>

Para integrar el SDK de Camera Kit de Snap con el SDK de transmisión para Android de IVS, debe instalar el SDK de Camera Kit, inicializar una sesión de Camera Kit, aplicar una lente y enviar el resultado de la sesión de Camera Kit a la fuente de entrada de la imagen personalizada.

Para instalar el SDK de Camera Kit, añada lo siguiente al archivo `build.gradle` de su módulo. Sustituya `$cameraKitVersion` por la [última versión del SDK del Camera Kit](https://docs.snap.com/camera-kit/integrate-sdk/mobile/changelog-mobile).

### Java
<a name="integrating-snap-android-install-camerakit-sdk-code"></a>

```
implementation "com.snap.camerakit:camerakit:$cameraKitVersion"
```

Inicialice y obtenga un `cameraKitSession`. Camera Kit también proporciona un práctico contenedor para las API [CameraX](https://developer.android.com/media/camera/camerax) de Android, por lo que no tiene que escribir una lógica complicada para usar CameraX con Camera Kit. Puede usar el objeto `CameraXImageProcessorSource` como [fuente](https://snapchat.github.io/camera-kit-reference/api/android/latest/-camera-kit/com.snap.camerakit/-source/index.html) para [ImageProcessor](https://snapchat.github.io/camera-kit-reference/api/android/latest/-camera-kit/com.snap.camerakit/-image-processor/index.html), que le permite iniciar fotogramas de transmisión con vistas previas de la cámara.

### Java
<a name="integrating-snap-android-initialize-camerakitsession-code"></a>

```
 protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        // Camera Kit support implementation of ImageProcessor that is backed by CameraX library:
        // https://developer.android.com/training/camerax
        CameraXImageProcessorSource imageProcessorSource = new CameraXImageProcessorSource( 
            this /*context*/, this /*lifecycleOwner*/
        );
        imageProcessorSource.startPreview(true /*cameraFacingFront*/);

        cameraKitSession = Sessions.newBuilder(this)
                .imageProcessorSource(imageProcessorSource)
                .attachTo(findViewById(R.id.camerakit_stub))
                .build();
    }
```

### Búsqueda y aplicación de lentes
<a name="integrating-snap-android-fetch-apply-lenses"></a>

Puede configurar las lentes y su orden en el carrusel del [portal para desarrolladores de Camera Kit](https://camera-kit.snapchat.com/):

#### Java
<a name="integrating-snap-android-configure-lenses-code"></a>

```
// Fetch lenses from repository and apply them
 // Replace LENS_GROUP_ID with Lens Group ID from https://camera-kit.snapchat.com
cameraKitSession.getLenses().getRepository().get(new Available(LENS_GROUP_ID), available -> {
     Log.d(TAG, "Available lenses: " + available);
     Lenses.whenHasFirst(available, lens -> cameraKitSession.getLenses().getProcessor().apply(lens, result -> {
          Log.d(TAG,  "Apply lens [" + lens + "] success: " + result);
      }));
});
```

Para transmitir, envíe los fotogramas procesados al `Surface` subyacente de una fuente de imagen personalizada. Use un objeto `DeviceDiscovery` y cree un `CustomImageSource` para devolver un`SurfaceSource`. A continuación, puede renderizar el resultado de una sesión de `CameraKit` en el `Surface` subyacente proporcionado por el`SurfaceSource`.

#### Java
<a name="integrating-snap-android-broadcast-code"></a>

```
val publishStreams = ArrayList<LocalStageStream>()

val deviceDiscovery = DeviceDiscovery(applicationContext)
val customSource = deviceDiscovery.createImageInputSource(BroadcastConfiguration.Vec2(720f, 1280f))

cameraKitSession.processor.connectOutput(outputFrom(customSource.inputSurface))
val customStream = ImageLocalStageStream(customSource)

// After rendering the output from a Camera Kit session to the Surface, you can 
// then return it as a LocalStageStream to be published by the Broadcast SDK
val customStream: ImageLocalStageStream = ImageLocalStageStream(surfaceSource)
publishStreams.add(customStream)

@Override
fun stageStreamsToPublishForParticipant(stage: Stage, participantInfo: ParticipantInfo): List<LocalStageStream> = publishStreams
```

# Uso del reemplazo de fondo con el SDK de transmisión de IVS
<a name="broadcast-3p-camera-filters-background-replacement"></a>

El reemplazo del fondo es un tipo de filtro de cámara que permite a los creadores de transmisiones en directo cambiar sus fondos. Como se muestra en el diagrama siguiente, reemplazar el fondo implica:

1. Obtener una imagen de cámara a partir de la transmisión de la cámara en directo.

1. Segmentarla en componentes de primer plano y segundo plano con el ML Kit de Google.

1. Combinar la máscara de segmentación resultante con una imagen de fondo personalizada.

1. Pasarla a una fuente de imagen personalizada para su transmisión.

![\[Flujo de trabajo para implementar el reemplazo del fondo.\]](http://docs.aws.amazon.com/es_es/ivs/latest/RealTimeUserGuide/images/3P_Camera_Filters_Background_Replacement.png)


## Web
<a name="background-replacement-web"></a>

En esta sección se presupone que ya está familiarizado con la [publicación de videos y la suscripción a ellos mediante el SDK de transmisión web](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/getting-started-pub-sub-web.html).

Para sustituir el fondo de una transmisión en directo por una imagen personalizada, utilice el [modelo de segmentación de selfies](https://developers.google.com/mediapipe/solutions/vision/image_segmenter#selfie-model) con [MediaPipe Image Segmenter](https://developers.google.com/mediapipe/solutions/vision/image_segmenter). Se trata de un modelo de machine learning que identifica qué píxeles del fotograma de video están en primer plano o en segundo plano. Puede utilizar los resultados del modelo para sustituir el fondo de una transmisión en directo copiando los píxeles de primer plano de la transmisión de video a una imagen personalizada que represente el nuevo fondo.

Para integrar el reemplazo del fondo de pantalla con el SDK de transmisión web en tiempo real de IVS, debe:

1. Instalar MediaPipe y Webpack. (Nuestro ejemplo usa Webpack como paquete, pero puede usar cualquier paquete de su elección).

1. Cree `index.html`.

1. Agregue elementos multimedia.

1. Añada una etiqueta de script.

1. Cree `app.js`.

1. Cargue una imagen de fondo personalizada.

1. Cree una instancia de `ImageSegmenter`.

1. Renderice la transmisión de video en un lienzo.

1. Cree una lógica de reemplazo de fondo.

1. Cree el archivo de configuración de Webpack.

1. Agrupe su archivo JavaScript.

### Instalación de MediaPipe y Webpack
<a name="background-replacement-web-install-mediapipe-webpack"></a>

Para empezar, instale `@mediapipe/tasks-vision` y los paquetes npm de `webpack`. El siguiente ejemplo usa Webpack como un empaquetador de JavaScript; puede usar un empaquetador diferente si lo prefiere.

#### JavaScript
<a name="background-replacement-web-install-mediapipe-webpack-code"></a>

```
npm i @mediapipe/tasks-vision webpack webpack-cli
```

Asegúrese de actualizar también su `package.json` para especificar `webpack` al desarrollar script:

#### JavaScript
<a name="background-replacement-web-update-package-json-code"></a>

```
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack"
  },
```

### Creación del archivo index.html
<a name="background-replacement-web-create-index"></a>

A continuación, cree la plantilla HTML e importe el SDK de transmisión web como una etiqueta script. En el código siguiente, asegúrese de reemplazar `<SDK version>` por la versión del SDK de transmisión que esté utilizando.

#### JavaScript
<a name="background-replacement-web-create-index-code"></a>

```
<!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/<SDK version>/amazon-ivs-web-broadcast.js"></script>
</head>

<body>

</body>
</html>
```

### Agregue elementos multimedia
<a name="background-replacement-web-add-media-elements"></a>

A continuación, agregue un elemento de video y dos elementos de lienzo dentro de la etiqueta corporal. El elemento de video contendrá las imágenes de la cámara en directo y se utilizará como entrada para el segmentador de imágenes de MediaPipe. El primer elemento de lienzo se utilizará para obtener una vista previa de la transmisión que se emitirá. El segundo elemento de lienzo se utilizará para renderizar la imagen personalizada que se utilizará como fondo. Como el segundo lienzo con la imagen personalizada solo se usa como fuente para copiar píxeles mediante programación al lienzo final, está oculto a la vista.

#### JavaScript
<a name="background-replacement-web-add-media-elements-code"></a>

```
<div class="row local-container">
      <video id="webcam" autoplay style="display: none"></video>
    </div>
    <div class="row local-container">
      <canvas id="canvas" width="640px" height="480px"></canvas>

      <div class="column" id="local-media"></div>
      <div class="static-controls hidden" id="local-controls">
        <button class="button" id="mic-control">Mute Mic</button>
        <button class="button" id="camera-control">Mute Camera</button>
      </div>
    </div>
    <div class="row local-container">
      <canvas id="background" width="640px" height="480px" style="display: none"></canvas>
    </div>
```

### Agregue una etiqueta de script
<a name="background-replacement-web-add-script-tag"></a>

Agregue una etiqueta de script para cargar un archivo JavaScript agrupado que contendrá el código para reemplazar el fondo y publicarlo en un escenario:

```
<script src="./dist/bundle.js"></script>
```

### Creación de app.js
<a name="background-replacement-web-create-appjs"></a>

A continuación, cree un archivo JavaScript para obtener los objetos de elemento para los elementos de lienzo y video que se crearon en la página HTML. Importe los módulos `ImageSegmenter` y `FilesetResolver`. El módulo `ImageSegmenter` se utilizará para realizar la tarea de segmentación.

#### JavaScript
<a name="create-appjs-import-imagesegmenter-fileresolver-code"></a>

```
const canvasElement = document.getElementById("canvas");
const background = document.getElementById("background");
const canvasCtx = canvasElement.getContext("2d");
const backgroundCtx = background.getContext("2d");
const video = document.getElementById("webcam");

import { ImageSegmenter, FilesetResolver } from "@mediapipe/tasks-vision";
```

A continuación, cree una función llamada `init()` para recuperar el MediaStream de la cámara del usuario e invoque una función de devolución de llamada cada vez que termine de cargarse el fotograma de la cámara. Añada detectores de eventos para que los botones se unan y salgan de un escenario.

Tenga en cuenta que cuando nos unimos a un escenario, pasamos una variable llamada`segmentationStream`. Se trata de una transmisión de video capturada desde un elemento de lienzo, que contiene una imagen de primer plano superpuesta a la imagen personalizada que representa el fondo. Más adelante, esta transmisión personalizada se utilizará para crear una instancia de `LocalStageStream`, que se podrá publicar en un escenario.

#### JavaScript
<a name="create-appjs-create-init-code"></a>

```
const init = async () => {
  await initializeDeviceSelect();

  cameraButton.addEventListener("click", () => {
    const isMuted = !cameraStageStream.isMuted;
    cameraStageStream.setMuted(isMuted);
    cameraButton.innerText = isMuted ? "Show Camera" : "Hide Camera";
  });

  micButton.addEventListener("click", () => {
    const isMuted = !micStageStream.isMuted;
    micStageStream.setMuted(isMuted);
    micButton.innerText = isMuted ? "Unmute Mic" : "Mute Mic";
  });

  localCamera = await getCamera(videoDevicesList.value);
  const segmentationStream = canvasElement.captureStream();

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

  leaveButton.addEventListener("click", () => {
    leaveStage();
  });
};
```

### Carga de una imagen de fondo personalizada
<a name="background-replacement-web-background-image"></a>

En la parte inferior de la función `init`, añada código para llamar a una función llamada`initBackgroundCanvas`, que carga una imagen personalizada de un archivo local y la renderiza en un lienzo. Definiremos esta función en el siguiente paso. Asigne la `MediaStream` recuperada de la cámara del usuario al objeto de video. Más tarde, este objeto de video se pasará al segmentador de imágenes. Además, configure una función denominada `renderVideoToCanvas` como la función de devolución de llamada para que se invoque cada vez que un fotograma de video termine de cargarse. Definiremos esta función en un paso posterior.

#### JavaScript
<a name="background-replacement-web-load-background-image-code"></a>

```
initBackgroundCanvas();

  video.srcObject = localCamera;
  video.addEventListener("loadeddata", renderVideoToCanvas);
```

Vamos a implementar la función `initBackgroundCanvas`, que carga una imagen desde un archivo local. En este ejemplo, utilizaremos una imagen de una playa como fondo personalizado. El lienzo que contiene la imagen personalizada se ocultará de la pantalla, ya que lo combinará con los píxeles del primer plano del elemento del lienzo que contiene la imagen de la cámara.

#### JavaScript
<a name="background-replacement-web-implement-initBackgroundCanvas-code"></a>

```
const initBackgroundCanvas = () => {
  let img = new Image();
  img.src = "beach.jpg";

  img.onload = () => {
    backgroundCtx.clearRect(0, 0, canvas.width, canvas.height);
    backgroundCtx.drawImage(img, 0, 0);
  };
};
```

### Creación de una instancia de ImageSegmenter
<a name="background-replacement-web-imagesegmenter"></a>

A continuación, cree una instancia de `ImageSegmenter`, que segmentará la imagen y devolverá el resultado en forma de máscara. Al crear una instancia de `ImageSegmenter`, utilizará el [modelo de segmentación de selfies](https://developers.google.com/mediapipe/solutions/vision/image_segmenter#selfie-model).

#### JavaScript
<a name="background-replacement-web-imagesegmenter-code"></a>

```
const createImageSegmenter = async () => {
  const audio = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.2/wasm");

  imageSegmenter = await ImageSegmenter.createFromOptions(audio, {
    baseOptions: {
      modelAssetPath: "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite",
      delegate: "GPU",
    },
    runningMode: "VIDEO",
    outputCategoryMask: true,
  });
};
```

### Renderice la transmisión de video en un lienzo
<a name="background-replacement-web-render-video-to-canvas"></a>

A continuación, cree la función que renderice la transmisión de video al otro elemento del lienzo. Necesitamos renderizar la transmisión de video en un lienzo para poder extraer los píxeles del primer plano utilizando la API Canvas 2D. Mientras lo hacemos, también pasaremos un fotograma de video a nuestra instancia`ImageSegmenter`, utilizando el método [segmentForVideo](https://developers.google.com/mediapipe/api/solutions/js/tasks-vision.imagesegmenter#imagesegmentersegmentforvideo) para segmentar el primer plano del fondo del fotograma de video. Cuando el método [segmentForVideo](https://developers.google.com/mediapipe/api/solutions/js/tasks-vision.imagesegmenter#imagesegmentersegmentforvideo) regrese, invoque nuestra función de devolución de llamada personalizada, `replaceBackground`, para reemplazar el fondo.

#### JavaScript
<a name="background-replacement-web-render-video-to-canvas-code"></a>

```
const renderVideoToCanvas = async () => {
  if (video.currentTime === lastWebcamTime) {
    window.requestAnimationFrame(renderVideoToCanvas);
    return;
  }
  lastWebcamTime = video.currentTime;
  canvasCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);

  if (imageSegmenter === undefined) {
    return;
  }

  let startTimeMs = performance.now();

  imageSegmenter.segmentForVideo(video, startTimeMs, replaceBackground);
};
```

### Creación de una lógica de reemplazo en segundo plano
<a name="background-replacement-web-logic"></a>

Cree la función `replaceBackground`, que fusiona la imagen de fondo personalizada con el primer plano de la transmisión de la cámara para reemplazar el fondo. La función recupera primero los datos de píxeles subyacentes de la imagen de fondo personalizada y la transmisión de video de los dos elementos del lienzo creados anteriormente. A continuación, recorre en iteración la máscara proporcionada por`ImageSegmenter`, que indica qué píxeles están en primer plano. A medida que recorre la máscara, copia de forma selectiva los píxeles que contienen la imagen de la cámara del usuario en los datos de píxeles de fondo correspondientes. Una vez hecho esto, convierte los datos de píxeles finales con el primer plano copiado en el fondo y los dibuja en un lienzo.

#### JavaScript
<a name="background-replacement-web-logic-create-replacebackground-code"></a>

```
function replaceBackground(result) {
  let imageData = canvasCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
  let backgroundData = backgroundCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
  const mask = result.categoryMask.getAsFloat32Array();
  let j = 0;

  for (let i = 0; i < mask.length; ++i) {
    const maskVal = Math.round(mask[i] * 255.0);

    j += 4;
  // Only copy pixels on to the background image if the mask indicates they are in the foreground
    if (maskVal < 255) {
      backgroundData[j] = imageData[j];
      backgroundData[j + 1] = imageData[j + 1];
      backgroundData[j + 2] = imageData[j + 2];
      backgroundData[j + 3] = imageData[j + 3];
    }
  }

 // Convert the pixel data to a format suitable to be drawn to a canvas
  const uint8Array = new Uint8ClampedArray(backgroundData.buffer);
  const dataNew = new ImageData(uint8Array, video.videoWidth, video.videoHeight);
  canvasCtx.putImageData(dataNew, 0, 0);
  window.requestAnimationFrame(renderVideoToCanvas);
}
```

Como referencia, aquí está el archivo `app.js` completo que contiene toda la lógica anterior:

#### JavaScript
<a name="background-replacement-web-logic-app-js-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

// All helpers are expose on 'media-devices.js' and 'dom.js'
const { setupParticipant } = window;

const { Stage, LocalStageStream, SubscribeType, StageEvents, ConnectionState, StreamType } = IVSBroadcastClient;
const canvasElement = document.getElementById("canvas");
const background = document.getElementById("background");
const canvasCtx = canvasElement.getContext("2d");
const backgroundCtx = background.getContext("2d");
const video = document.getElementById("webcam");

import { ImageSegmenter, FilesetResolver } from "@mediapipe/tasks-vision";

let cameraButton = document.getElementById("camera-control");
let micButton = document.getElementById("mic-control");
let joinButton = document.getElementById("join-button");
let leaveButton = document.getElementById("leave-button");

let controls = document.getElementById("local-controls");
let audioDevicesList = document.getElementById("audio-devices");
let videoDevicesList = document.getElementById("video-devices");

// Stage management
let stage;
let joining = false;
let connected = false;
let localCamera;
let localMic;
let cameraStageStream;
let micStageStream;
let imageSegmenter;
let lastWebcamTime = -1;

const init = async () => {
  await initializeDeviceSelect();

  cameraButton.addEventListener("click", () => {
    const isMuted = !cameraStageStream.isMuted;
    cameraStageStream.setMuted(isMuted);
    cameraButton.innerText = isMuted ? "Show Camera" : "Hide Camera";
  });

  micButton.addEventListener("click", () => {
    const isMuted = !micStageStream.isMuted;
    micStageStream.setMuted(isMuted);
    micButton.innerText = isMuted ? "Unmute Mic" : "Mute Mic";
  });

  localCamera = await getCamera(videoDevicesList.value);
  const segmentationStream = canvasElement.captureStream();

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

  leaveButton.addEventListener("click", () => {
    leaveStage();
  });

  initBackgroundCanvas();

  video.srcObject = localCamera;
  video.addEventListener("loadeddata", renderVideoToCanvas);
};

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

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

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

  // Retrieve the User Media currently set on the page
  localMic = await getMic(audioDevicesList.value);

  cameraStageStream = new LocalStageStream(segmentationStream.getVideoTracks()[0]);
  micStageStream = new LocalStageStream(localMic.getAudioTracks()[0]);

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

  stage = new Stage(token, strategy);

  // Other available events:
  // https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#events
  stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {
    connected = state === ConnectionState.CONNECTED;

    if (connected) {
      joining = false;
      controls.classList.remove("hidden");
    } else {
      controls.classList.add("hidden");
    }
  });

  stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {
    console.log("Participant Joined:", participant);
  });

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

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

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

  joining = false;
  connected = false;

  cameraButton.innerText = "Hide Camera";
  micButton.innerText = "Mute Mic";
  controls.classList.add("hidden");
};

function replaceBackground(result) {
  let imageData = canvasCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
  let backgroundData = backgroundCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
  const mask = result.categoryMask.getAsFloat32Array();
  let j = 0;

  for (let i = 0; i < mask.length; ++i) {
    const maskVal = Math.round(mask[i] * 255.0);

    j += 4;
    if (maskVal < 255) {
      backgroundData[j] = imageData[j];
      backgroundData[j + 1] = imageData[j + 1];
      backgroundData[j + 2] = imageData[j + 2];
      backgroundData[j + 3] = imageData[j + 3];
    }
  }
  const uint8Array = new Uint8ClampedArray(backgroundData.buffer);
  const dataNew = new ImageData(uint8Array, video.videoWidth, video.videoHeight);
  canvasCtx.putImageData(dataNew, 0, 0);
  window.requestAnimationFrame(renderVideoToCanvas);
}

const createImageSegmenter = async () => {
  const audio = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.2/wasm");

  imageSegmenter = await ImageSegmenter.createFromOptions(audio, {
    baseOptions: {
      modelAssetPath: "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite",
      delegate: "GPU",
    },
    runningMode: "VIDEO",
    outputCategoryMask: true,
  });
};

const renderVideoToCanvas = async () => {
  if (video.currentTime === lastWebcamTime) {
    window.requestAnimationFrame(renderVideoToCanvas);
    return;
  }
  lastWebcamTime = video.currentTime;
  canvasCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);

  if (imageSegmenter === undefined) {
    return;
  }

  let startTimeMs = performance.now();

  imageSegmenter.segmentForVideo(video, startTimeMs, replaceBackground);
};

const initBackgroundCanvas = () => {
  let img = new Image();
  img.src = "beach.jpg";

  img.onload = () => {
    backgroundCtx.clearRect(0, 0, canvas.width, canvas.height);
    backgroundCtx.drawImage(img, 0, 0);
  };
};

createImageSegmenter();
init();
```

### Creación de un archivo de configuración de Webpack
<a name="background-replacement-web-webpack-config"></a>

Agregue esta configuración a su archivo de configuración de Webpack para empaquetar`app.js`, de modo que las llamadas de importación funcionen:

#### JavaScript
<a name="background-replacement-web-webpack-config-code"></a>

```
const path = require("path");
module.exports = {
  entry: ["./app.js"],
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
};
```

### Agrupación de sus archivos JavaScript
<a name="background-replacement-web-bundle-javascript"></a>

```
npm run build
```

Inicie un servidor HTTP simple desde el directorio que contiene `index.html` y abra `localhost:8000` para ver el resultado:

```
python3 -m http.server -d ./
```

## Android
<a name="background-replacement-android"></a>

Para reemplazar el fondo de tu transmisión en directo, puede usar la API de segmentación de selfies del [ML Kit de Google](https://developers.google.com/ml-kit/vision/selfie-segmentation). La API de segmentación de selfies acepta una imagen de cámara como entrada y devuelve una máscara que proporciona una puntuación de confianza para cada píxel de la imagen, que indica si estaba en primer plano o en segundo plano. En función de la puntuación de confianza, puede recuperar el color de píxel correspondiente de la imagen de fondo o de la imagen de primer plano. Este proceso continúa hasta que se hayan examinado todas las puntuaciones de confianza de la máscara. El resultado es una nueva matriz de colores de píxeles que contiene los píxeles del primer plano combinados con los píxeles de la imagen de fondo.

Para integrar la sustitución en segundo plano con el SDK de retransmisión para transmisión en tiempo real de IVS:

1. Instale las bibliotecas CameraX y el kit ML de Google.

1. Inicialice las variables repetitivas.

1. Cree de una fuente de imágenes personalizada.

1. Administre los fotogramas de las cámaras.

1. Transfiera los marcos de las cámaras al ML Kit de Google.

1. Superponga el primer plano del marco de la cámara sobre su fondo personalizado.

1. Introduzca la nueva imagen en una fuente de imágenes personalizada.

### Instalación de las bibliotecas CameraX y del kit ML de Google
<a name="background-replacement-android-install-camerax-googleml"></a>

Para extraer imágenes de la transmisión de la cámara en vivo, use la biblioteca CameraX de Android. Para instalar la biblioteca CameraX y el kit ML de Google, añada lo siguiente al archivo `build.gradle` del módulo. Sustituya `${camerax_version}` y `${google_ml_kit_version}` por la última versión de las bibliotecas [CameraX](https://developer.android.com/jetpack/androidx/releases/camera) y [ML Kit Google](https://developers.google.com/ml-kit/vision/selfie-segmentation/android), respectivamente. 

#### Java
<a name="background-replacement-android-install-camerax-googleml-code"></a>

```
implementation "com.google.mlkit:segmentation-selfie:${google_ml_kit_version}"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
```

Importe las siguientes bibliotecas:

#### Java
<a name="background-replacement-android-import-libraries-code"></a>

```
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider
import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions
```

### Inicialización de variables reutilizables
<a name="background-replacement-android-initialize-variables"></a>

Inicialice una instancia de `ImageAnalysis` y una instancia de `ExecutorService`:

#### Java
<a name="background-replacement-android-initialize-imageanalysis-executorservice-code"></a>

```
private lateinit var binding: ActivityMainBinding
private lateinit var cameraExecutor: ExecutorService
private var analysisUseCase: ImageAnalysis? = null
```

[Inicialice una instancia en STREAM\$1MODE](https://developers.google.com/ml-kit/vision/selfie-segmentation/android#detector_mode):

#### Java
<a name="background-replacement-android-initialize-segmenter-code"></a>

```
private val options =
        SelfieSegmenterOptions.Builder()
            .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
            .build()

private val segmenter = Segmentation.getClient(options)
```

### Creación de una fuente de imágenes personalizada
<a name="background-replacement-android-create-image-source"></a>

En el método `onCreate` de su actividad, cree una instancia de un objeto `DeviceDiscovery` y cree una fuente de imágenes personalizada. El `Surface` proporcionado por la fuente de imagen personalizada recibirá la imagen final, con el primer plano superpuesto sobre una imagen de fondo personalizada. A continuación, creará una instancia de `ImageLocalStageStream` utilizando la fuente de imagen personalizada. Luego, la instancia de `ImageLocalStageStream` (nombrada `filterStream` en este ejemplo) puede publicarse en un escenario. Consulte la [Guía del SDK de transmisión para Android de IVS](broadcast-android.md) para obtener instrucciones sobre cómo configurar un escenario. Por último, cree también un hilo que se utilizará para gestionar la cámara.

#### Java
<a name="background-replacement-android-create-image-source-code"></a>

```
var deviceDiscovery = DeviceDiscovery(applicationContext)
var customSource = deviceDiscovery.createImageInputSource( BroadcastConfiguration.Vec2(
720F, 1280F
))
var surface: Surface = customSource.inputSurface
var filterStream = ImageLocalStageStream(customSource)

cameraExecutor = Executors.newSingleThreadExecutor()
```

### Administración de fotogramas de cámara
<a name="background-replacement-android-camera-frames"></a>

A continuación, cree una función para inicializar la cámara. Esta función utiliza la biblioteca CameraX para extraer imágenes de la transmisión de la cámara en directo. En primer lugar, se crea una instancia de `ProcessCameraProvider` llamada `cameraProviderFuture`. Este objeto representa un resultado futuro de la obtención de un proveedor de cámaras. A continuación, cargue una imagen del proyecto en forma de mapa de bits. En este ejemplo se utiliza la imagen de una playa como fondo, pero puede ser cualquier imagen que desee.

A continuación, añada un oyente a `cameraProviderFuture`. Este oyente recibe una notificación cuando la cámara está disponible o si se produce un error durante el proceso de obtención de un proveedor de cámaras.

#### Java
<a name="background-replacement-android-initialize-camera-code"></a>

```
private fun startCamera(surface: Surface) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        val imageResource = R.drawable.beach
        val bgBitmap: Bitmap = BitmapFactory.decodeResource(resources, imageResource)
        var resultBitmap: Bitmap;


        cameraProviderFuture.addListener({
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            
                if (mediaImage != null) {
                    val inputImage =
                        InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

                            resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
                            canvas = surface.lockCanvas(null);
                            canvas.drawBitmap(resultBitmap, 0f, 0f, null)

                            surface.unlockCanvasAndPost(canvas);

                        }
                        .addOnFailureListener { exception ->
                            Log.d("App", exception.message!!)
                        }
                        .addOnCompleteListener {
                            imageProxy.close()
                        }

                }
            };

            val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase)

            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }
```

En el oyente, cree `ImageAnalysis.Builder` para acceder a cada fotograma individual de la transmisión de la cámara en directo. Establezca la estrategia de contrapresión para `STRATEGY_KEEP_ONLY_LATEST`. Esto garantiza que solo se entregue un cuadro de cámara a la vez para su procesamiento. Convierta cada fotograma de cámara individual en un mapa de bits, de forma que pueda extraer sus píxeles y luego combinarlos con la imagen de fondo personalizada.

#### Java
<a name="background-replacement-android-create-imageanalysisbuilder-code"></a>

```
val imageAnalyzer = ImageAnalysis.Builder()
analysisUseCase = imageAnalyzer
    .setTargetResolution(Size(360, 640))
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()

analysisUseCase?.setAnalyzer(cameraExecutor) { imageProxy: ImageProxy ->
    val mediaImage = imageProxy.image
    val tempBitmap = imageProxy.toBitmap();
    val inputBitmap = tempBitmap.rotate(imageProxy.imageInfo.rotationDegrees.toFloat())
```

### Transferencia de los marcos de cámara al ML Kit de Google
<a name="background-replacement-android-frames-to-mlkit"></a>

A continuación, cree una `InputImage` y pásela a la instancia de Segmenter para su procesamiento. Se puede crear una `InputImage` a partir de una `ImageProxy` proporcionada por la instancia de `ImageAnalysis`. Una vez que se proporciona una `InputImage` a Segmenter, devuelve una máscara con puntuaciones de confianza que indican la probabilidad de que un píxel esté en primer plano o en segundo plano. Esta máscara también proporciona propiedades de ancho y alto, que utilizará para crear una nueva matriz que contenga los píxeles de fondo de la imagen de fondo personalizada cargada anteriormente.

#### Java
<a name="background-replacement-android-frames-to-mlkit-code"></a>

```
if (mediaImage != null) {
        val inputImage =
            InputImage.fromMediaImag


segmenter.process(inputImage)
    .addOnSuccessListener { segmentationMask ->
        val mask = segmentationMask.buffer
        val maskWidth = segmentationMask.width
        val maskHeight = segmentationMask.height
        val backgroundPixels = IntArray(maskWidth * maskHeight)
        bgBitmap.getPixels(backgroundPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)
```

### Superposición del primer plano del marco de la cámara sobre el fondo personalizado
<a name="background-replacement-android-overlay-frame-foreground"></a>

Con la máscara que contiene las puntuaciones de confianza, el marco de la cámara como mapa de bits y los píxeles de color de la imagen de fondo personalizada, tiene todo lo que necesita para superponer el primer plano al fondo personalizado. A continuación, se invoca la función `overlayForeground` con los siguientes parámetros:

#### Java
<a name="background-replacement-android-call-overlayforeground-code"></a>

```
resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
```

Esta función recorre la máscara y comprueba los valores de confianza para determinar si se debe obtener el color de píxel correspondiente de la imagen de fondo o del marco de la cámara. Si el valor de confianza indica que lo más probable es que un píxel de la máscara esté en segundo plano, obtendrá el color de píxel correspondiente de la imagen de fondo; de lo contrario, obtendrá el color de píxel correspondiente del marco de la cámara para crear el primer plano. Una vez que la función termine de recorrer la máscara, se crea un nuevo mapa de bits con la nueva matriz de píxeles de color y se devuelve. Este nuevo mapa de bits contiene el primer plano superpuesto sobre el fondo personalizado.

#### Java
<a name="background-replacement-android-run-overlayforeground-code"></a>

```
private fun overlayForeground(
        byteBuffer: ByteBuffer,
        maskWidth: Int,
        maskHeight: Int,
        cameraBitmap: Bitmap,
        backgroundPixels: IntArray
    ): Bitmap {
        @ColorInt val colors = IntArray(maskWidth * maskHeight)
        val cameraPixels = IntArray(maskWidth * maskHeight)

        cameraBitmap.getPixels(cameraPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)

        for (i in 0 until maskWidth * maskHeight) {
            val backgroundLikelihood: Float = 1 - byteBuffer.getFloat()

            // Apply the virtual background to the color if it's not part of the foreground
            if (backgroundLikelihood > 0.9) {
                // Get the corresponding pixel color from the background image
                // Set the color in the mask based on the background image pixel color
                colors[i] = backgroundPixels.get(i)
            } else {
                // Get the corresponding pixel color from the camera frame
                // Set the color in the mask based on the camera image pixel color
                colors[i] = cameraPixels.get(i)
            }
        }

        return Bitmap.createBitmap(
            colors, maskWidth, maskHeight, Bitmap.Config.ARGB_8888
        )
    }
```

### Introducción de la nueva imagen en una fuente de imagen personalizada
<a name="background-replacement-android-custom-image-source"></a>

A continuación, puede escribir el nuevo mapa de bits en el `Surface` proporcionado por una fuente de imagen personalizada. Esto se transmitirá a su escenario.

#### Java
<a name="background-replacement-android-custom-image-source-code"></a>

```
resultBitmap = overlayForeground(mask, inputBitmap, mutableBitmap, bgBitmap)
canvas = surface.lockCanvas(null);
canvas.drawBitmap(resultBitmap, 0f, 0f, null)
```

Esta es la función completa para obtener los fotogramas de la cámara, pasarlos a Segmenter y superponerlos sobre el fondo:

#### Java
<a name="background-replacement-android-custom-image-source-startcamera-code"></a>

```
@androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class)
    private fun startCamera(surface: Surface) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        val imageResource = R.drawable.clouds
        val bgBitmap: Bitmap = BitmapFactory.decodeResource(resources, imageResource)
        var resultBitmap: Bitmap;

        cameraProviderFuture.addListener({
            // Used to bind the lifecycle of cameras to the lifecycle owner
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            val imageAnalyzer = ImageAnalysis.Builder()
            analysisUseCase = imageAnalyzer
                .setTargetResolution(Size(720, 1280))
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()

            analysisUseCase!!.setAnalyzer(cameraExecutor) { imageProxy: ImageProxy ->
                val mediaImage = imageProxy.image
                val tempBitmap = imageProxy.toBitmap();
                val inputBitmap = tempBitmap.rotate(imageProxy.imageInfo.rotationDegrees.toFloat())

                if (mediaImage != null) {
                    val inputImage =
                        InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

                    segmenter.process(inputImage)
                        .addOnSuccessListener { segmentationMask ->
                            val mask = segmentationMask.buffer
                            val maskWidth = segmentationMask.width
                            val maskHeight = segmentationMask.height
                            val backgroundPixels = IntArray(maskWidth * maskHeight)
                            bgBitmap.getPixels(backgroundPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)

                            resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
                            canvas = surface.lockCanvas(null);
                            canvas.drawBitmap(resultBitmap, 0f, 0f, null)

                            surface.unlockCanvasAndPost(canvas);

                        }
                        .addOnFailureListener { exception ->
                            Log.d("App", exception.message!!)
                        }
                        .addOnCompleteListener {
                            imageProxy.close()
                        }

                }
            };

            val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase)

            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }
```

# SDK de transmisión de IVS: modos de audio móvil \$1 Transmisión en tiempo real
<a name="broadcast-mobile-audio-modes"></a>

La calidad del audio es una parte importante de la experiencia multimedia de cualquier equipo real, y no existe una configuración de audio única que funcione mejor para todos los casos de uso. Para garantizar que sus usuarios disfruten de la mejor experiencia al escuchar una transmisión en tiempo real de IVS, nuestros SDK para dispositivos móviles ofrecen varias configuraciones de audio predefinidas, así como personalizaciones más potentes, según sea necesario.

## Introducción
<a name="broadcast-mobile-audio-modes-introduction"></a>

Los SDK de transmisión móvil de IVS ofrecen una clase `StageAudioManager`. Esta clase está diseñada para ser el único punto de contacto para controlar los modos de audio subyacentes en ambas plataformas. En Android, esto controla el [AudioManager](https://developer.android.com/reference/android/media/AudioManager), incluidos el modo de audio, la fuente de audio, el tipo de contenido, el uso y los dispositivos de comunicación. En iOS, controla la aplicación [AVAudioSession](https://developer.apple.com/documentation/avfaudio/avaudiosession), así como si [VoiceProcessing](https://developer.apple.com/documentation/avfaudio/avaudioionode/3152101-voiceprocessingenabled?language=objc) está habilitado.

**Importante**: no interactúe con `AVAudioSession` ni `AudioManager` directamente mientras el SDK de transmisión en tiempo real de IVS esté activo. Si lo hace, podría perderse el audio o grabarse o reproducirse en el dispositivo incorrecto.

Antes de crear el primer objeto `DeviceDiscovery` o `Stage`, debe estar configurada la clase `StageAudioManager`.

------
#### [ Android (Kotlin) ]

```
StageAudioManager.getInstance(context).setPreset(StageAudioManager.UseCasePreset.VIDEO_CHAT) // The default value

val deviceDiscovery = DeviceDiscovery(context)
val stage = Stage(context, token, this)

// Other Stage implementation code
```

------
#### [ iOS (Swift) ]

```
IVSStageAudioManager.sharedInstance().setPreset(.videoChat) // The default value

let deviceDiscovery = IVSDeviceDiscovery()
let stage = try? IVSStage(token: token, strategy: self)

// Other Stage implementation code
```

------

Si no hay nada establecido en `StageAudioManager` antes de la inicialización de una instancia `DeviceDiscovery` o `Stage`, el ajuste preestablecido `VideoChat` se aplica automáticamente.

## Modo preestablecido de audio
<a name="broadcast-mobile-audio-modes-presets"></a>

El SDK de transmisión en tiempo real proporciona tres ajustes preestablecidos, cada uno adaptado a los casos de uso más comunes, tal y como se describe a continuación. Para cada ajuste preestablecido, cubrimos cinco categorías clave que diferencian los ajustes preestablecidos entre sí.

La categoría **Control de volumen** hace referencia al tipo de volumen (volumen de contenido multimedia o volumen de llamadas) que se utiliza o se cambia mediante los controles de volumen físicos del dispositivo. Tenga en cuenta que esto afecta al volumen al cambiar de modo de audio. Por ejemplo, supongamos que el volumen del dispositivo está ajustado al valor máximo mientras se utiliza el ajuste preestablecido de chat de video. Al cambiar al ajuste preestablecido Suscripción única, el nivel de volumen es diferente al del sistema operativo, lo que podría provocar un cambio de volumen significativo en el dispositivo.

### Chat de vídeo
<a name="audio-modes-presets-video-chat"></a>

Este es el ajuste preestablecido predeterminado, diseñado para cuando el dispositivo local va a mantener una conversación en tiempo real con otros participantes.

**Problema conocido en iOS**: al usar este ajuste preestablecido y no conectar un micrófono, el audio se reproduce a través del auricular en lugar del altavoz del dispositivo. Use este ajuste preestablecido solo en combinación con un micrófono.


| Categoría | Android | iOS | 
| --- | --- | --- | 
| Cancelación de eco | Habilitado | Habilitado | 
| Control de volumen | Volumen de llamadas | Volumen de llamadas | 
| Selección de micrófono | Limitado según el sistema operativo. Es posible que los micrófonos USB no estén disponibles. | Limitado según el sistema operativo. Es posible que los micrófonos USB y Bluetooth no estén disponibles. Deberían funcionar los auriculares Bluetooth que admiten tanto la entrada como la salida al mismo tiempo; por ejemplo, los AirPods. | 
| Salida de audio | Cualquier dispositivo de salida debería funcionar. | Limitado según el sistema operativo. Es posible que los auriculares con cable no estén disponibles. | 
| Calidad de audio | Media/baja. Sonará como una llamada telefónica, no como una reproducción multimedia. | Media/baja. Sonará como una llamada telefónica, no como una reproducción multimedia. | 

### Suscripción única
<a name="audio-modes-presets-subscribe-only"></a>

Este ajuste preestablecido está diseñado para suscribir a otros participantes de la publicación, pero no para publicar usted mismo. Se centra en la calidad del audio y es compatible con todos los dispositivos de salida disponibles.


| Categoría | Android | iOS | 
| --- | --- | --- | 
| Cancelación de eco | Deshabilitad | Deshabilitado | 
| Control de volumen | Volumen multimedia | Volumen multimedia | 
| Selección de micrófono | N/A, este ajuste preestablecido no está diseñado para su publicación. | N/A, este ajuste preestablecido no está diseñado para su publicación. | 
| Salida de audio | Cualquier dispositivo de salida debería funcionar. | Cualquier dispositivo de salida debería funcionar. | 
| Calidad de audio | Alta. Cualquier tipo multimedia debería mostrarse con claridad, incluida la música. | Alta. Cualquier tipo multimedia debería mostrarse con claridad, incluida la música. | 

### Studio
<a name="audio-modes-presets-studio"></a>

Este ajuste preestablecido está diseñado para una suscripción de alta calidad y, al mismo tiempo, permite publicar. Requiere el hardware de grabación y reproducción para poder cancelar el eco. Un caso de uso en este caso sería usar un micrófono USB y unos auriculares con cable. El SDK mantendrá la más alta calidad de audio y, al mismo tiempo, se basará en la separación física de esos dispositivos para evitar que produzcan eco.


| Categoría | Android | iOS | 
| --- | --- | --- | 
| Cancelación de eco | La cancelación de eco de la plataforma está deshabilitada, pero la cancelación de eco por software puede seguir produciéndose si la `StageAudioConfiguration.enableEchoCancellation` se mantiene. | Deshabilitado | 
| Control de volumen | Volumen multimedia en la mayoría de los casos. Volumen de llamadas cuando hay un micrófono Bluetooth conectado.  | Volumen multimedia | 
| Selección de micrófono | Cualquier micrófono debería funcionar. | Cualquier micrófono debería funcionar. | 
| Salida de audio | Cualquier dispositivo de salida debería funcionar. | Cualquier dispositivo de salida debería funcionar. | 
| Calidad de audio | Alta. Ambos lados deberían poder enviar música y escucharla con claridad en el otro lado. Cuando se conecta un auricular Bluetooth, la calidad del audio disminuirá debido a que el modo Bluetooth SCO está activado. | Alta. Ambos lados deberían poder enviar música y escucharla con claridad en el otro lado. Cuando se conecta un auricular Bluetooth, la calidad del audio puede disminuir debido a la activación del modo Bluetooth SCO, en función del auricular.  | 

## Casos de uso avanzados
<a name="broadcast-mobile-audio-modes-advanced-use-cases"></a>

Más allá de los ajustes preestablecidos, los SDK de transmisión en tiempo real de iOS y Android permiten configurar los modos de audio de la plataforma subyacente:
+ En Android, configure [AudioSource](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource), [Usage](https://developer.android.com/reference/android/media/AudioAttributes#USAGE_ALARM) y [ContentType](https://developer.android.com/reference/android/media/AudioAttributes#CONTENT_TYPE_MOVIE).
+ En iOS, use [AVAudioSession.Category](https://developer.apple.com/documentation/avfaudio/avaudiosession/category), [AVAudioSession.CategoryOptions](https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions), [AVAudioSession.Mode](https://developer.apple.com/documentation/avfaudio/avaudiosession/mode) y la posibilidad de activar o no el [procesamiento de voz](https://developer.apple.com/documentation/avfaudio/avaudioionode/3152101-voiceprocessingenabled?language=objc) durante la publicación.

Nota: Al utilizar estos métodos del SDK de audio, es posible configurar incorrectamente la sesión de audio subyacente. Por ejemplo, si se utiliza la opción `.allowBluetooth` en iOS en combinación con la categoría `.playback`, se crea una configuración de audio no válida y el SDK no puede grabar ni reproducir audio. Estos métodos están diseñados para usarse solo cuando una aplicación tiene requisitos específicos de sesión de audio que se han validado.

------
#### [ Android (Kotlin) ]

```
// This would act similar to the Subscribe Only preset, but it uses a different ContentType.
StageAudioManager.getInstance(context)
    .setConfiguration(StageAudioManager.Source.GENERIC,
                      StageAudioManager.ContentType.MOVIE,
                      StageAudioManager.Usage.MEDIA);

val stage = Stage(context, token, this)

// Other Stage implementation code
```

------
#### [ iOS (Swift) ]

```
// This would act similar to the Subscribe Only preset, but it uses a different mode and options.
IVSStageAudioManager.sharedInstance()
    .setCategory(.playback,
                 options: [.duckOthers, .mixWithOthers],
                 mode: .default)

let stage = try? IVSStage(token: token, strategy: self)

// Other Stage implementation code
```

------

### Cancelación de eco de iOS
<a name="advanced-use-cases-ios_echo_cancellation"></a>

La cancelación de eco de iOS también se puede controlar de forma independiente a través de `IVSStageAudioManager` y mediante el método `echoCancellationEnabled`. Este método controla si el [procesamiento de voz](https://developer.apple.com/documentation/avfaudio/avaudioionode/3152101-voiceprocessingenabled?language=objc) está habilitado en los nodos de entrada y salida de la propiedad subyacente `AVAudioEngine` utilizada por el SDK. Es importante comprender el efecto de cambiar esta propiedad de forma manual:
+ La propiedad `AVAudioEngine` solo se respeta si el micrófono del SDK está activo; esto es necesario debido al requisito de iOS de que el procesamiento de voz esté habilitado simultáneamente en los nodos de entrada y salida. Normalmente, esto se hace con el micrófono devuelto por `IVSDeviceDiscovery` para crear un objeto `IVSLocalStageStream` y publicarlo. Como alternativa, el micrófono se puede habilitar, sin que se utilice para publicar, mediante la asociación de un objeto `IVSAudioDeviceStatsCallback` al propio micrófono. Este enfoque alternativo resulta útil si se necesita cancelar el eco al utilizar un micrófono basado en un origen de audio personalizado en lugar del micrófono del SDK del IVS.
+ Para habilitar la propiedad `AVAudioEngine`, se requiere el modo `.videoChat` o `.voiceChat`. Al solicitar otro modo, la estructura de audio subyacente de iOS se opone al SDK, lo que provoca la pérdida de audio.
+ Al habilitar `AVAudioEngine`, la opción `.allowBluetooth ` se habilita automáticamente.

Los comportamientos pueden variar según el dispositivo y la versión de iOS.

### Orígenes de audio personalizados para iOS
<a name="advanced-use-cases-ios_custom_audio_sources"></a>

Los orígenes de audio personalizados se pueden usar con el SDK mediante `IVSDeviceDiscovery.createAudioSource`. Al conectarse a una fase, el SDK de retransmisión en tiempo real de IVS sigue administrando una instancia de `AVAudioEngine` interna para la reproducción de audio, aunque no se utilice el micrófono del SDK. Como resultado, los valores proporcionados a `IVSStageAudioManager` deben ser compatibles con el audio que proporciona el origen de audio personalizado.

Si el origen de audio personalizado que se utiliza para publicar graba desde el micrófono pero lo administra la aplicación host, el SDK de cancelación de eco anterior no funcionará a menos que el micrófono administrado por el SDK esté activado. Para evitar ese requisito, consulte [Cancelación de eco de iOS](#advanced-use-cases-ios_echo_cancellation).

### Publicación con Bluetooth en Android
<a name="advanced-use-cases-bluetooth-android"></a>

El SDK vuelve automáticamente al `VIDEO_CHAT` preestablecido en Android cuando se cumplen las siguientes condiciones:
+ La configuración asignada no usa el valor de uso `VOICE_COMMUNICATION`.
+ Hay un micrófono Bluetooth conectado al dispositivo.
+ El participante local está publicando en un escenario.

Esta es una limitación del sistema operativo Android en lo que respecta al uso de auriculares Bluetooth para grabar audio.

## Integración con otros servicios de SDK
<a name="broadcast-mobile-audio-modes-integrating-other-sdks"></a>

Como tanto iOS como Android admiten solo un modo de audio activo por aplicación, es habitual que surjan conflictos si la aplicación utiliza varios SDK que requieren el control del modo de audio. Cuando se encuentre con estos conflictos, puede probar algunas estrategias de resolución habituales, que se explican a continuación.

### Coincidencia de los valores del modo de audio
<a name="integrating-other-sdks-match-values"></a>

Con las opciones de configuración de audio avanzadas del SDK de IVS o las funciones de otro SDK, haga que los dos SDK se alineen con los valores subyacentes.

### Ágora
<a name="integrating-other-sdks-agora"></a>

#### iOS
<a name="integrating-other-sdks-agora-ios"></a>

En iOS, decirle al SDK de Agora que mantenga la `AVAudioSession` activa evitará que se desactive mientras el SDK de transmisión en tiempo real de IVS lo esté utilizando.

```
myRtcEngine.SetParameters("{\"che.audio.keep.audiosession\":true}");
```

#### Android
<a name="integrating-other-sdks-agora-android"></a>

Evita llamar `setEnableSpeakerphone` en `RtcEngine`, y llame a `enableLocalAudio(false)` mientras publica con el SDK de transmisión en tiempo real de IVS. Puede volver a llamar a `enableLocalAudio(true)` cuando el SDK de IVS no esté publicándose.