Seleccione sus preferencias de cookies

Usamos cookies esenciales y herramientas similares que son necesarias para proporcionar nuestro sitio y nuestros servicios. Usamos cookies de rendimiento para recopilar estadísticas anónimas para que podamos entender cómo los clientes usan nuestro sitio y hacer mejoras. Las cookies esenciales no se pueden desactivar, pero puede hacer clic en “Personalizar” o “Rechazar” para rechazar las cookies de rendimiento.

Si está de acuerdo, AWS y los terceros aprobados también utilizarán cookies para proporcionar características útiles del sitio, recordar sus preferencias y mostrar contenido relevante, incluida publicidad relevante. Para aceptar o rechazar todas las cookies no esenciales, haga clic en “Aceptar” o “Rechazar”. Para elegir opciones más detalladas, haga clic en “Personalizar”.

Publicación y suscripción con el SDK de transmisión para iOS de IVS - Amazon IVS

Publicación y suscripción con el SDK de transmisión para iOS de IVS

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

Creación de vistas

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

import AmazonIVSBroadcast class ViewController: UIViewController { @IBOutlet private var textFieldToken: UITextField! @IBOutlet private var buttonJoin: UIButton! @IBOutlet private var labelState: UILabel! @IBOutlet private var switchPublish: UISwitch! @IBOutlet private var collectionViewParticipants: UICollectionView!

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

Use Main.storyboard para crear una vista de iOS.

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

Personalice la Vista de colección de participantes de iOS.

La segunda vista es Contenedor de controles. Alinee InicialFinal y Superior con respecto a Área segura:

Personalice la vista Contenedor de controles de iOS.

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

Personalice la vista de pila vertical de iOS.

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

Personalice el resto de las vistas de iOS con UIStackViews.

Por último, vinculemos estas vistas a nuestro ViewController. Desde arriba, asigne las siguientes vistas:

  • El campo de texto de unión se asigna a textFieldToken.

  • El botón de unión se asigna a buttonJoin.

  • La etiqueta de estado se asigna a labelState.

  • El botón de publicación se asigna a switchPublish.

  • Vista de colección de participantes se asigna a collectionViewParticipants.

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

Configure el dataSource de Vista de colección de participantes de la aplicación de iOS.

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

Cree una UICollectionViewCell para renderizar los participantes de iOS en tiempo real.

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

import AmazonIVSBroadcast class ParticipantCollectionViewCell: UICollectionViewCell { @IBOutlet private var viewPreviewContainer: UIView! @IBOutlet private var labelParticipantId: UILabel! @IBOutlet private var labelSubscribeState: UILabel! @IBOutlet private var labelPublishState: UILabel! @IBOutlet private var labelVideoMuted: UILabel! @IBOutlet private var labelAudioMuted: UILabel! @IBOutlet private var labelAudioVolume: UILabel!

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

Cree una jerarquía de vistas de iOS en el archivo XIB asociado.

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

Personalice la Vista previa de contenedores de iOS.

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

Personalice la vista de iOS.

La tercera vista es Vista de pila. Alinee FinalInicialSuperior e Inferior con respecto a Supervista y cambie el valor a 4.

Personalice la vista de pila de iOS.

Permisos y temporizador de inactividad

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

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

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

private func checkPermissions() { checkOrGetPermission(for: .video) { [weak self] granted in guard granted else { print("Video permission denied") return } self?.checkOrGetPermission(for: .audio) { [weak self] granted in guard granted else { print("Audio permission denied") return } self?.setupLocalUser() // we will cover this later } } } private func checkOrGetPermission(for mediaType: AVMediaType, _ result: @escaping (Bool) -> Void) { func mainThreadResult(_ success: Bool) { DispatchQueue.main.async { result(success) } } switch AVCaptureDevice.authorizationStatus(for: mediaType) { case .authorized: mainThreadResult(true) case .notDetermined: AVCaptureDevice.requestAccess(for: mediaType) { granted in mainThreadResult(granted) } case .denied, .restricted: mainThreadResult(false) @unknown default: mainThreadResult(false) } }

Estado de la aplicación

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

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

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

import Foundation import AmazonIVSBroadcast struct StageParticipant { let isLocal: Bool var participantId: String? var publishState: IVSParticipantPublishState = .notPublished var subscribeState: IVSParticipantSubscribeState = .notSubscribed var streams: [IVSStageStream] = [] init(isLocal: Bool, participantId: String?) { self.isLocal = isLocal self.participantId = participantId } }

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

private var participants = [StageParticipant]()

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

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

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

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

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

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

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

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

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

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

ChangeType es una enumeración simple:

enum ChangeType { case joined, updated, left }

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

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

Implementación del SDK de escenarios

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

IVSStageStrategy

La implementación de IVSStageStrategy es sencilla:

extension ViewController: IVSStageStrategy { func stage(_ stage: IVSStage, streamsToPublishForParticipant participant: IVSParticipantInfo) -> [IVSLocalStageStream] { // Return the camera and microphone to be published. // This is only called if `shouldPublishParticipant` returns true. return streams } func stage(_ stage: IVSStage, shouldPublishParticipant participant: IVSParticipantInfo) -> Bool { // Our publish status is based directly on the UISwitch view return switchPublish.isOn } func stage(_ stage: IVSStage, shouldSubscribeToParticipant participant: IVSParticipantInfo) -> IVSStageSubscribeType { // Subscribe to both audio and video for all publishing participants. return .audioVideo } }

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

IVSStageRenderer

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

extension ViewController: IVSStageRenderer { func stage(_ stage: IVSStage, didChange connectionState: IVSStageConnectionState, withError error: Error?) { labelState.text = connectionState.text connectingOrConnected = connectionState != .disconnected } func stage(_ stage: IVSStage, participantDidJoin participant: IVSParticipantInfo) { if participant.isLocal { // If this is the local participant joining the Stage, update the first participant in our array because we // manually added that participant when setting up our preview participants[0].participantId = participant.participantId participantsChanged(index: 0, changeType: .updated) } else { // If they are not local, add them to the array as a newly joined participant. participants.append(StageParticipant(isLocal: false, participantId: participant.participantId)) participantsChanged(index: (participants.count - 1), changeType: .joined) } } func stage(_ stage: IVSStage, participantDidLeave participant: IVSParticipantInfo) { if participant.isLocal { // If this is the local participant leaving the Stage, update the first participant in our array because // we want to keep the camera preview active participants[0].participantId = nil participantsChanged(index: 0, changeType: .updated) } else { // If they are not local, find their index and remove them from the array. if let index = participants.firstIndex(where: { $0.participantId == participant.participantId }) { participants.remove(at: index) participantsChanged(index: index, changeType: .left) } } } func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChange publishState: IVSParticipantPublishState) { // Update the publishing state of this participant mutatingParticipant(participant.participantId) { data in data.publishState = publishState } } func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChange subscribeState: IVSParticipantSubscribeState) { // Update the subscribe state of this participant mutatingParticipant(participant.participantId) { data in data.subscribeState = subscribeState } } func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChangeMutedStreams streams: [IVSStageStream]) { // We don't want to take any action for the local participant because we track those streams locally if participant.isLocal { return } // For remote participants, notify the UICollectionView that they have updated. There is no need to modify // the `streams` property on the `StageParticipant` because it is the same `IVSStageStream` instance. Just // query the `isMuted` property again. if let index = participants.firstIndex(where: { $0.participantId == participant.participantId }) { participantsChanged(index: index, changeType: .updated) } } func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didAdd streams: [IVSStageStream]) { // We don't want to take any action for the local participant because we track those streams locally if participant.isLocal { return } // For remote participants, add these new streams to that participant's streams array. mutatingParticipant(participant.participantId) { data in data.streams.append(contentsOf: streams) } } func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didRemove streams: [IVSStageStream]) { // We don't want to take any action for the local participant because we track those streams locally if participant.isLocal { return } // For remote participants, remove these streams from that participant's streams array. mutatingParticipant(participant.participantId) { data in let oldUrns = streams.map { $0.device.descriptor().urn } data.streams.removeAll(where: { stream in return oldUrns.contains(stream.device.descriptor().urn) }) } } // A helper function to find a participant by its ID, mutate that participant, and then update the UICollectionView accordingly. private func mutatingParticipant(_ participantId: String?, modifier: (inout StageParticipant) -> Void) { guard let index = participants.firstIndex(where: { $0.participantId == participantId }) else { fatalError("Something is out of sync, investigate if this was a sample app or SDK issue.") } var participant = participants[index] modifier(&participant) participants[index] = participant participantsChanged(index: index, changeType: .updated) } }

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

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

Implementación de UICollectionViewLayout personalizado

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

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

import Foundation import UIKit /** Code modified from https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/layouts/customizing_collection_view_layouts?language=objc */ class ParticipantCollectionViewLayout: UICollectionViewLayout { private let layoutCalculator = StageLayoutCalculator() private var contentBounds = CGRect.zero private var cachedAttributes = [UICollectionViewLayoutAttributes]() override func prepare() { super.prepare() guard let collectionView = collectionView else { return } cachedAttributes.removeAll() contentBounds = CGRect(origin: .zero, size: collectionView.bounds.size) layoutCalculator.calculateFrames(participantCount: collectionView.numberOfItems(inSection: 0), width: collectionView.bounds.size.width, height: collectionView.bounds.size.height, padding: 4) .enumerated() .forEach { (index, frame) in let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: index, section: 0)) attributes.frame = frame cachedAttributes.append(attributes) contentBounds = contentBounds.union(frame) } } override var collectionViewContentSize: CGSize { return contentBounds.size } override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { guard let collectionView = collectionView else { return false } return !newBounds.size.equalTo(collectionView.bounds.size) } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return cachedAttributes[indexPath.item] } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var attributesArray = [UICollectionViewLayoutAttributes]() // Find any cell that sits within the query rect. guard let lastIndex = cachedAttributes.indices.last, let firstMatchIndex = binSearch(rect, start: 0, end: lastIndex) else { return attributesArray } // Starting from the match, loop up and down through the array until all the attributes // have been added within the query rect. for attributes in cachedAttributes[..<firstMatchIndex].reversed() { guard attributes.frame.maxY >= rect.minY else { break } attributesArray.append(attributes) } for attributes in cachedAttributes[firstMatchIndex...] { guard attributes.frame.minY <= rect.maxY else { break } attributesArray.append(attributes) } return attributesArray } // Perform a binary search on the cached attributes array. func binSearch(_ rect: CGRect, start: Int, end: Int) -> Int? { if end < start { return nil } let mid = (start + end) / 2 let attr = cachedAttributes[mid] if attr.frame.intersects(rect) { return mid } else { if attr.frame.maxY < rect.minY { return binSearch(rect, start: (mid + 1), end: end) } else { return binSearch(rect, start: start, end: (mid - 1)) } } } }

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

import Foundation import UIKit class StageLayoutCalculator { /// This 2D array contains the description of how the grid of participants should be rendered /// The index of the 1st dimension is the number of participants needed to active that configuration /// Meaning if there is 1 participant, index 0 will be used. If there are 5 participants, index 4 will be used. /// /// The 2nd dimension is a description of the layout. The length of the array is the number of rows that /// will exist, and then each number within that array is the number of columns in each row. /// /// See the code comments next to each index for concrete examples. /// /// This can be customized to fit any layout configuration needed. private let layouts: [[Int]] = [ // 1 participant [ 1 ], // 1 row, full width // 2 participants [ 1, 1 ], // 2 rows, all columns are full width // 3 participants [ 1, 2 ], // 2 rows, first row's column is full width then 2nd row's columns are 1/2 width // 4 participants [ 2, 2 ], // 2 rows, all columns are 1/2 width // 5 participants [ 1, 2, 2 ], // 3 rows, first row's column is full width, 2nd and 3rd row's columns are 1/2 width // 6 participants [ 2, 2, 2 ], // 3 rows, all column are 1/2 width // 7 participants [ 2, 2, 3 ], // 3 rows, 1st and 2nd row's columns are 1/2 width, 3rd row's columns are 1/3rd width // 8 participants [ 2, 3, 3 ], // 9 participants [ 3, 3, 3 ], // 10 participants [ 2, 3, 2, 3 ], // 11 participants [ 2, 3, 3, 3 ], // 12 participants [ 3, 3, 3, 3 ], ] // Given a frame (this could be for a UICollectionView, or a Broadcast Mixer's canvas), calculate the frames for each // participant, with optional padding. func calculateFrames(participantCount: Int, width: CGFloat, height: CGFloat, padding: CGFloat) -> [CGRect] { if participantCount > layouts.count { fatalError("Only \(layouts.count) participants are supported at this time") } if participantCount == 0 { return [] } var currentIndex = 0 var lastFrame: CGRect = .zero // If the height is less than the width, the rows and columns will be flipped. // Meaning for 6 participants, there will be 2 rows of 3 columns each. let isVertical = height > width let halfPadding = padding / 2.0 let layout = layouts[participantCount - 1] // 1 participant is in index 0, so `-1`. let rowHeight = (isVertical ? height : width) / CGFloat(layout.count) var frames = [CGRect]() for row in 0 ..< layout.count { // layout[row] is the number of columns in a layout let itemWidth = (isVertical ? width : height) / CGFloat(layout[row]) let segmentFrame = CGRect(x: (isVertical ? 0 : lastFrame.maxX) + halfPadding, y: (isVertical ? lastFrame.maxY : 0) + halfPadding, width: (isVertical ? itemWidth : rowHeight) - padding, height: (isVertical ? rowHeight : itemWidth) - padding) for column in 0 ..< layout[row] { var frame = segmentFrame if isVertical { frame.origin.x = (itemWidth * CGFloat(column)) + halfPadding } else { frame.origin.y = (itemWidth * CGFloat(column)) + halfPadding } frames.append(frame) currentIndex += 1 } lastFrame = segmentFrame lastFrame.origin.x += halfPadding lastFrame.origin.y += halfPadding } return frames } }

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

Xcode interface showing storyboard with UICollectionView and its layout settings.

Enlace de acciones de la interfaz de usuario

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

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

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

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

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

Renderización de los participantes

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

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

func set(participant: StageParticipant) { }

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

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

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

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

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

// This belongs outside `set(participant:)` private var registeredStreams: Set<IVSStageStream> = [] private var imageDevice: IVSImageDevice? { return registeredStreams.lazy.compactMap { $0.device as? IVSImageDevice }.first } private var audioDevice: IVSAudioDevice? { return registeredStreams.lazy.compactMap { $0.device as? IVSAudioDevice }.first } // This belongs inside `set(participant:)` let existingAudioStream = registeredStreams.first { $0.device is IVSAudioDevice } let existingImageStream = registeredStreams.first { $0.device is IVSImageDevice } registeredStreams = Set(participant.streams) let newAudioStream = participant.streams.first { $0.device is IVSAudioDevice } let newImageStream = participant.streams.first { $0.device is IVSImageDevice } // `isMuted != false` covers the stream not existing, as well as being muted. labelVideoMuted.text = "Video Muted: \(newImageStream?.isMuted != false)" labelAudioMuted.text = "Audio Muted: \(newAudioStream?.isMuted != false)"

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

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

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

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

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

extension UIView { func addSubviewMatchFrame(_ view: UIView) { view.translatesAutoresizingMaskIntoConstraints = false self.addSubview(view) NSLayoutConstraint.activate([ view.topAnchor.constraint(equalTo: self.topAnchor, constant: 0), view.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0), view.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0), view.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0), ]) } }
PrivacidadTérminos del sitioPreferencias de cookies
© 2025, Amazon Web Services, Inc o sus afiliados. Todos los derechos reservados.