使用 IVS iOS 廣播 SDK 發布和訂閱
本節將引導您完成使用 iOS 應用程式發布和訂閱階段的相關步驟。
建立檢視
首先,使用自動建立用於匯入 AmazonIVSBroadcast
的 ViewController.swift
檔案,然後新增一些要連結的 @IBOutlets
:
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!
現在,建立這些檢視,並在 Main.storyboard
中將其連結。以下是將使用的檢視結構:
若是 AutoLayout 組態,需要自訂三個檢視。第一個檢視是集合檢視參與者 (UICollectionView
)。將前導、尾隨和底部繫結至安全區域。同時將頂部繫結至控制容器。
第二個檢視是控制容器。將前導、尾隨和頂部繫結至安全區域。
第三個和最後一個檢視是垂直堆疊檢視。將頂部、前導、尾隨和底部繫結至父檢視。針對樣式,將間距設定為 8,而不是 0。
UIStackViews 會處理其餘檢視的配置。針對所有三個 UIStackViews,在對齊和分佈時使用填滿。
最後,將這些檢視連結至 ViewController
。從上方映射下列檢視:
-
文字欄位加入繫結至
textFieldToken
。 -
按鈕加入繫結至
buttonJoin
。 -
標籤狀態繫結至
labelState
。 -
切換發布繫結至
switchPublish
。 -
集合檢視參與者繫結至
collectionViewParticipants
。
同時還會將集合檢視參與者項目的 dataSource
設定為擁有的 ViewController
:
現在,建立要在其中轉譯參與者的 UICollectionViewCell
子類別。首先,建立一個新的 Cocoa Touch 類別檔案:
將其命名為 ParticipantUICollectionViewCell
,然後使其成為 Swift 中 UICollectionViewCell
的子類別。再次從 Swift 檔案開始,建立要連結的 @IBOutlets
:
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!
在關聯的 XIB 檔案中,建立此檢視階層:
針對 AutoLayout,將再次修改三個檢視。第一個檢視是檢視預覽容器。將尾隨、前導、頂部和底部設定為參與者集合檢視儲存格。
第二個檢視是檢視。將前導和頂部設定為參與者集合檢視儲存格,並將該值變更為 4。
第三個檢視是堆疊檢視。將尾隨、前導、頂部和底部設定為父檢視,並將該值變更為 4。
許可和閒置計時器
回到 ViewController
,將停用系統閒置計時器,以免在使用應用程式時,裝置進入睡眠狀態:
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 }
接著,請求系統的攝影機和麥克風許可:
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) } }
應用程式狀態
需要設定 collectionViewParticipants
以及之前建立的配置檔案:
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") }
為了代表每個參與者,我們建立了一個稱為 StageParticipant
的簡單結構。這可包含在 ViewController.swift
檔案中,或建立新的檔案。
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 } }
為了追蹤這些參與者,我們將其陣列做為私有屬性保留在 ViewController
中:
private var participants = [StageParticipant]()
此屬性將用於支援之前從分鏡腳本連結的 UICollectionViewDataSource
:
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'") } } }
若要在加入階段之前查看您自己的預覽,我們會立即建立本機參與者:
override func viewDidLoad() { /* existing UICollectionView code */ participants.append(StageParticipant(isLocal: true, participantId: nil)) }
這會導致在應用程式執行後,代表本機參與者立即轉譯參與者儲存格。
使用者希望能夠在加入階段之前看到自己,因此,接下來我們實作 setupLocalUser()
方法,可從稍早的許可處理程式碼中呼叫。將攝影機和麥克風參考做為 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) }
在這裡透過 SDK 找到了裝置的攝影機和麥克風,並將其存放在本機 streams
物件中,然後將第一個參與者 (我們之前建立的本機參與者) 的 streams
陣列指派到 streams
。最後,呼叫 index
為 0,以及 changeType
為 updated
的 participantsChanged
。該函數是透過良好的動畫更新 UICollectionView
的輔助程式函數。這是呈現的外觀:
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)]) } }
別擔心 cell.set
;稍後會處理,這就是我們將根據參與者轉譯儲存格內容的地方。
ChangeType
是一個簡單的列舉:
enum ChangeType { case joined, updated, left }
最後,想要追蹤階段是否已建立連線。使用一個簡單的 bool
來追蹤,在其自行更新時,會自動更新 UI。
private var connectingOrConnected = false { didSet { buttonJoin.setTitle(connectingOrConnected ? "Leave" : "Join", for: .normal) buttonJoin.tintColor = connectingOrConnected ? .systemRed : .systemBlue } }
實作階段 SDK
以下是三個以即時功能為基礎的核心概念:階段、策略和轉譯器。設計目標是盡可能減少打造工作產品所需的用戶端邏輯數量。
IVSStageStrategy
IVSStageStrategy
實作很簡單:
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 } }
總而言之,只有在發布切換按鈕處於「開啟」位置時才會發布,如果要發布,我們會發布之前收集的串流。最後,針對此範例,始終會訂閱其他參與者,同時接收其音訊和影片。
IVSStageRenderer
IVSStageRenderer
實作也非常簡單,但要考慮包含相當多程式碼的函數數量。當 SDK 通知我們有關參與者的變更時,此轉譯器中的一般方法是更新我們的 participants
陣列。在某些情況下,對於本機參與者的處理方式有所不同,因為我們已決定自行管理他們,以便在他們加入之前查看其攝影機預覽。
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) } }
此程式碼使用延伸,將連線狀態轉換為易讀的文字:
extension IVSStageConnectionState { var text: String { switch self { case .disconnected: return "Disconnected" case .connecting: return "Connecting" case .connected: return "Connected" @unknown default: fatalError() } } }
實作自訂 UICollectionViewLayout
配置不同數量的參與者可能很複雜。您希望他們佔用整個父檢視的框架,但不想單獨處理每個參與者配置。為了簡化這一點,我們將逐步實作 UICollectionViewLayout
。
建立另一個新檔案 ParticipantCollectionViewLayout.swift
,這應當會延伸 UICollectionViewLayout
。此類別會使用另一個稱為 StageLayoutCalculator
類別,我們很快會介紹。該類別會接收針對每個參與者計算的影格值,然後產生必要的 UICollectionViewLayoutAttributes
物件。
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)) } } } }
更重要的是 StageLayoutCalculator.swift
類別。設計此類別的目的是,根據以流程為基礎的資料列/資料欄配置中的參與者人數,來計算每個參與者的影格。每個資料列的高度與其他資料列相同,但每個資料列的寬度可能有所差異。請參閱 layouts
變數上方的程式碼註解,描述了如何自訂此行為。
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 } }
回到 Main.storyboard
,請務必將 UICollectionView
的配置類別設定為我們剛才建立的類別:
掛接 UI 動作
我們即將結束,只需建立幾個 IBActions
。
首先,將處理加入按鈕。其反應因 connectingOrConnected
的值而異。一旦建立連線後,便會離開階段。如果中斷連線,則會從權杖 UITextField
讀取文字,並建立具有該文字的新的 IVSStage
。然後,新增 ViewController
做為 strategy
、errorDelegate
和用於 IVSStage
的轉譯器,最後,我們以非同步方式加入階段。
@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)") } } }
需要掛接的另一個 UI 動作是發布切換按鈕:
@IBAction private func publishToggled(_ sender: UISwitch) { // Because the strategy returns the value of `switchPublish.isOn`, just call `refreshStrategy`. stage?.refreshStrategy() }
轉譯參與者
最後,我們需要將從 SDK 接收的資料,轉譯到之前建立的參與者儲存格上。我們已經完成了 UICollectionView
邏輯,因此只需實作 ParticipantCollectionViewCell.swift
中的 set
API。
從新增 empty
函數開始,然後逐步實作:
func set(participant: StageParticipant) { }
首先,處理簡易狀態、參與者 ID、發布狀態和訂閱狀態。對於這些,只需直接更新 UILabels
:
labelParticipantId.text = participant.isLocal ? "You (\(participant.participantId ?? "Disconnected"))" : participant.participantId labelPublishState.text = participant.publishState.text labelSubscribeState.text = participant.subscribeState.text
發布和訂閱列舉的文字屬性源自本機延伸:
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() } } }
接著,更新音訊和影片靜音狀態。為取得靜音狀態,需要從 streams
陣列中找到 IVSImageDevice
和 IVSAudioDevice
。為優化效能,會記住上次連接的裝置。
// 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)"
最後,要轉譯 imageDevice
的預覽,並顯示 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" }
我們需要建立的最後一個函數是 updatePreview()
,這會將參與者的預覽新增至檢視:
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) } } }
上面使用了 UIView
上的輔助程式函數,使內嵌子檢視變得更容易:
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), ]) } }