Publicar e assinar com o SDK de Transmissão para iOS do IVS - Amazon IVS

Publicar e assinar com o SDK de Transmissão para iOS do IVS

Esta seção descreve as etapas envolvidas na publicação e assinatura de um estágio usando a sua aplicação para iOS.

Criar visualizações

Começamos usando o arquivo ViewController.swift criado automaticamente para importar AmazonIVSBroadcast e, em seguida, adicionamos alguns @IBOutlets para 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!

Agora, criamos essas visualizações e as vinculamos no Main.storyboard. Esta é a estrutura de visualização que usaremos:

Uso de Main.storyboard para a criação de uma visualização do iOS.

Para a configuração do AutoLayout, precisamos personalizar três visualizações. A primeira visualização corresponde a Collection View Participants (uma UICollectionView). Vincule Leading, Trailing e Bottom à Safe Area. Também vincule Top ao Controls Container.

Personalização da visualização Collection View Participants do iOS.

A segunda visualização corresponde ao Controls Container. Vincule Leading, Trailing e Top à Safe Area:

Personalização da visualização do Controls Container do iOS.

A terceira e última visualização corresponde a Vertical Stack View. Vincule Top, Leading, Trailing e Bottom à Superview. Para estilizar, defina o espaçamento para 8, em vez de 0.

Personalização da visualização Vertical Stack do iOS.

A UIStackViews lidará com o layout das visualizações restantes. Para todas as três UIStackViews, use Fill como Alignment e Distribution.

Personalização das visualizações restantes do iOS com a UIStackViews.

Por fim, vincularemos essas visualizações ao nosso ViewController. Depois da etapa acima, mapeie as seguintes visualizações:

  • Text Field Join é vinculado a textFieldToken.

  • Button Join é vinculado a buttonJoin.

  • Label State é vinculado a labelState.

  • Switch Publish é vinculada a switchPublish.

  • Collection View Participants é vinculada a collectionViewParticipants.

Aproveite também para definir a dataSource do item Collection View Participants para o ViewController proprietário:

Definição da dataSource da visualização Collection View Participants para a aplicação iOS.

Agora, criaremos a subclasse UICollectionViewCell na qual renderizaremos os participantes. Comece com a criação de um novo arquivo Cocoa Touch Class:

Criação de uma UICollectionViewCell para renderizar os participantes em tempo real do iOS.

Nomeie-o como ParticipantUICollectionViewCell e torne-o uma subclasse de UICollectionViewCell no Swift. Começamos no arquivo Swift novamente, criando nosso @IBOutlets para 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!

No arquivo XIB associado, crie esta hierarquia de visualização:

Criação de uma hierarquia de visualização do iOS no arquivo XIB associado.

Para o AutoLayout, modificaremos as três visualizações novamente. A primeira visualização corresponde a View Preview Container. Defina Trailing, Leading, Top e Bottom para a Participant Collection View Cell.

Personalização da visualização View Preview Container do iOS.

A segunda visualização corresponde a View. Defina Leading e Top para a Participant Collection View Cell e altere o valor para 4.

Personalização da visualização View do iOS.

A terceira visualização corresponde a Stack View. Defina Trailing, Leading, Top e Bottom para a Superview e altere o valor para 4.

Personalização da visualização Stack View do iOS.

Permissões e temporizador de ociosidade

Retornando ao nosso ViewController, desabilitaremos o temporizador de ociosidade do sistema para evitar que o dispositivo entre em hibernação enquanto nossa aplicação estiver sendo utilizada:

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 }

Em seguida, solicitamos permissões de câmera e de microfone por parte do 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 da aplicação

Precisamos configurar nosso collectionViewParticipants com o arquivo de layout que criamos 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 cada participante, criamos uma estrutura simples chamada StageParticipant. Isso pode ser incluso no arquivo ViewController.swift ou um novo arquivo pode ser criado.

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 acompanhar esses participantes, mantemos uma variedade deles como propriedade privada em nosso ViewController:

private var participants = [StageParticipant]()

Essa propriedade será usada para potencializar nosso UICollectionViewDataSource que foi vinculado do roteiro visual anteriormente:

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 sua própria visualização prévia antes de ingressar em um palco, criamos um participante local imediatamente:

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

Isso resulta na renderização de uma célula de participante imediatamente após a execução da aplicação, representando o participante local.

Os usuários querem ver a si mesmos antes de ingressar em um palco, por isso, em seguida, implementamos o método setupLocalUser() que foi chamado pelo código de gerenciamento de permissões anteriormente. Armazenamos a referência da câmera e do microfone como objetos 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) }

Aqui encontramos a câmera e o microfone do dispositivo por meio do SDK e os armazenamos em nosso objeto streams local e, em seguida, atribuímos a matriz de streams do primeiro participante (o participante local que criamos anteriormente) aos nossos streams. Por fim, chamamos participantsChanged com um index de 0 e changeType de updated. Essa função é uma função auxiliar para atualizar nosso UICollectionView com animações bonitas. Ela será algo assim:

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

Não se preocupe com cell.set ainda; abordaremos isso mais tarde, mas é aí que renderizaremos o conteúdo da célula com base no participante.

O ChangeType é uma enumeração simples:

enum ChangeType { case joined, updated, left }

Por fim, desejamos acompanhar se o palco está conectado. Usamos um simples bool para acompanhar isso, que atualizará automaticamente nossa interface do usuário quando ela for atualizada.

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

Implementar o SDK do palco

Existem três conceitos principais que fundamentam a funcionalidade em tempo real: palco, estratégia e renderizador. O objetivo do projeto é minimizar a quantidade de lógica do lado do cliente que é necessária para desenvolver um produto funcional.

IVSStageStrategy

Nossa implementação de IVSStageStrategy é simples:

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

Para resumir, realizaremos publicações somente se o botão de publicação estiver na posição “ligado” e, se publicarmos, usaremos os streams que coletamos anteriormente. Por fim, para esta amostra, sempre nos inscrevemos em outros participantes, recebendo seus áudios e vídeos.

IVSStageRenderer

A implementação de IVSStageRenderer também é bastante simples, embora, devido ao número de funções, contenha um pouco mais de código. A abordagem geral neste renderizador é atualizar nossa matriz de participants quando o SDK nos notifica sobre uma alteração em um participante. Existem certos cenários nos quais lidamos com os participantes locais de maneira diferente, pois decidimos gerenciá-los nós mesmos para que eles possam ver a visualização prévia da câmera antes de ingressar.

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 uma extensão para converter o estado da conexão em um texto amigável:

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

Implementação de um UICollectionViewLayout personalizado

A organização de diferentes números de participantes pode ser complexa. Você deseja que eles ocupem todo o quadro da visualização primária, mas não quer lidar com a configuração de cada participante independentemente. Para facilitar isso, abordaremos a implementação de um UICollectionViewLayout.

Crie outro novo arquivo, ParticipantCollectionViewLayout.swift, que deve abranger o UICollectionViewLayout. Essa classe usará outra classe chamada StageLayoutCalculator, que abordaremos em breve. A classe recebe os valores de quadros calculados para cada participante e, em seguida, gera os objetos UICollectionViewLayoutAttributes necessários.

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

A classe mais importante é a StageLayoutCalculator.swift. Ela foi projetada para calcular os quadros de cada participante com base no número de participantes em um layout de linha/coluna baseado em fluxo. Cada linha tem a mesma altura que as outras, mas as colunas podem ter larguras diferentes por linha. Consulte o comentário do código acima da variável layouts para obter uma descrição de como personalizar esse comportamento.

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

De volta ao Main.storyboard, certifique-se de definir a classe de layout de UICollectionView para a classe que acabamos de criar:

Xcode interface showing storyboard with UICollectionView and its layout settings.

Conexão de ações da interface do usuário

Estamos quase finalizando, mas há algumas IBActions que precisamos criar.

Primeiro, abordaremos o botão Ingressar. Ele responde de forma diferente com base no valor de connectingOrConnected. Quando tudo já está conectado, ele apenas deixa o palco. Se houver desconexão, ele lê o texto do token UITextField e cria um novo IVSStage com esse texto. Em seguida, adicionamos nosso ViewController como strategy, errorDelegate e renderizador para o IVSStage e, finalmente, ingressamos no palco de forma assí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)") } } }

A outra ação da interface do usuário que precisamos conectar é a opção de publicação:

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

Renderização dos participantes

Por fim, precisamos renderizar os dados que recebemos do SDK na célula do participante que criamos anteriormente. Já temos a lógica do UICollectionView finalizada, então só precisamos implementar a API set em ParticipantCollectionViewCell.swift.

Começaremos com a adição da função empty e, em seguida, abordaremos ela detalhadamente:

func set(participant: StageParticipant) { }

Primeiro, tratamos do estado fácil, do ID do participante, do estado de publicação e do estado de inscrição. Para esses, apenas atualizamos nosso UILabels diretamente:

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

As propriedades de texto das enumerações de publicação e inscrição vêm de extensões locais:

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

Em seguida, atualizamos os estados de áudio e vídeo silenciados. Para obter os estados silenciados, precisamos encontrar o IVSImageDevice e o IVSAudioDevice da matriz de streams. Para otimizar a performance, lembraremos dos ú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 fim, desejamos renderizar uma visualização prévia para o imageDevice e exibir as estatísticas de áudio do 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" }

A última função que precisamos criar é updatePreview(), que adiciona uma visualização prévia do participante à nossa visualização:

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

O código acima usa uma função auxiliar na UIView para facilitar a incorporação de subvisualizações:

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