Carlos Santos aeb6990aba Added Livekit ios example
Integrated token request using an application server

Downgraded features for adapting others tutorials

ios: Updated assets and remove unncessary code

Updated schemas and app name

Added configure urls view and refactored code

ios: Refactored code

removed broadcastExt

renamed project to OpenViduIOS

refactored code

Updated Readme

removed connection time element

Added participants name and moved leave room button to topbar

Refactored code

Update README.md
Renamed and improve project structure
2024-08-07 12:37:13 +02:00

386 lines
13 KiB
Swift

/*
* Copyright 2024 LiveKit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import LiveKit
import SFSafeSymbols
import SwiftUI
#if !os(macOS)
let adaptiveMin = 170.0
let toolbarPlacement: ToolbarItemPlacement = .bottomBar
#else
let adaptiveMin = 300.0
let toolbarPlacement: ToolbarItemPlacement = .primaryAction
#endif
extension CIImage {
// helper to create a `CIImage` for both platforms
convenience init(named name: String) {
#if !os(macOS)
self.init(cgImage: UIImage(named: name)!.cgImage!)
#else
self.init(data: NSImage(named: name)!.tiffRepresentation!)!
#endif
}
}
#if os(macOS)
// keeps weak reference to NSWindow
class WindowAccess: ObservableObject {
private weak var window: NSWindow?
deinit {
// reset changed properties
DispatchQueue.main.async { [weak window] in
window?.level = .normal
}
}
@Published public var pinned: Bool = false {
didSet {
guard oldValue != pinned else { return }
level = pinned ? .floating : .normal
}
}
private var level: NSWindow.Level {
get { window?.level ?? .normal }
set {
Task { @MainActor in
window?.level = newValue
objectWillChange.send()
}
}
}
public func set(window: NSWindow?) {
self.window = window
Task { @MainActor in
objectWillChange.send()
}
}
}
#endif
struct RoomView: View {
@EnvironmentObject var appCtx: AppContext
@EnvironmentObject var roomCtx: RoomContext
@EnvironmentObject var room: Room
@State var isCameraPublishingBusy = false
@State var isMicrophonePublishingBusy = false
@State var isScreenSharePublishingBusy = false
@State private var screenPickerPresented = false
@State private var publishOptionsPickerPresented = false
@State private var cameraPublishOptions = VideoPublishOptions()
#if os(macOS)
@ObservedObject private var windowAccess = WindowAccess()
#endif
func sortedParticipants() -> [Participant] {
room.allParticipants.values.sorted { p1, p2 in
if p1 is LocalParticipant { return true }
if p2 is LocalParticipant { return false }
return (p1.joinedAt ?? Date()) < (p2.joinedAt ?? Date())
}
}
func content(geometry: GeometryProxy) -> some View {
VStack {
HStack {
// Title Text
Text(roomCtx.name)
.font(.title2)
.fontWeight(.bold)
.multilineTextAlignment(.center)
.foregroundColor(.white)
.padding()
Spacer() // Pushes the button to the right
Button(action: {
Task {
await roomCtx.disconnect()
}
}, label: {
HStack {
Image(systemSymbol: .xmarkCircleFill)
.renderingMode(.original)
Text("Leave Room")
.font(.headline)
.fontWeight(.semibold)
}
.padding(8)
.background(Color.red.opacity(0.8)) // Background color for the button
.foregroundColor(.white) // Text color
.cornerRadius(8)
})
//.padding()
}
// Re-connecting Status
if case .connecting = room.connectionState {
Text("Re-connecting...")
.font(.subheadline)
.foregroundColor(.white)
.padding()
.background(Color.black.opacity(0.6))
.cornerRadius(8)
.padding(.bottom)
}
// Participant layout
HorVStack(axis: geometry.isTall ? .vertical : .horizontal, spacing: 5) {
Group {
ParticipantLayout(sortedParticipants(), spacing: 5) { participant in
ParticipantView(participant: participant, videoViewMode: .fill)
}
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
)
}
.padding(5)
}
}
var body: some View {
GeometryReader { geometry in
content(geometry: geometry)
}
.toolbar {
ToolbarItemGroup(placement: toolbarPlacement) {
HStack {
Spacer()
let isMicrophoneEnabled = room.localParticipant.isMicrophoneEnabled()
let isCameraEnabled = room.localParticipant.isCameraEnabled()
Button(action: {
Task {
isCameraPublishingBusy = true
defer { Task { @MainActor in isCameraPublishingBusy = false } }
do {
try await room.localParticipant.setCamera(enabled: !isCameraEnabled)
} catch {
print("Failed to toggle camera: \(error)")
}
}
}, label: {
Image(systemSymbol: isCameraEnabled ? .videoFill : .videoSlashFill)
.renderingMode(isCameraEnabled ? .original : .template)
})
.disabled(isCameraPublishingBusy)
Button(action: {
Task {
isMicrophonePublishingBusy = true
defer { Task { @MainActor in isMicrophonePublishingBusy = false } }
do {
try await room.localParticipant.setMicrophone(enabled: !isMicrophoneEnabled)
} catch {
print("Failed to toggle microphone: \(error)")
}
}
},
label: {
Image(systemSymbol: isMicrophoneEnabled ? .micFill : .micSlashFill)
.renderingMode(isMicrophoneEnabled ? .original : .template)
})
.disabled(isMicrophonePublishingBusy)
Spacer()
}
}
}
}
}
struct ParticipantLayout<Content: View>: View {
let views: [AnyView]
let spacing: CGFloat
init<Data: RandomAccessCollection>(
_ data: Data,
id: KeyPath<Data.Element, Data.Element> = \.self,
spacing: CGFloat,
@ViewBuilder content: @escaping (Data.Element) -> Content
) {
self.spacing = spacing
views = data.map { AnyView(content($0[keyPath: id])) }
}
func computeColumn(with geometry: GeometryProxy) -> (x: Int, y: Int) {
let sqr = Double(views.count).squareRoot()
let r: [Int] = [Int(sqr.rounded()), Int(sqr.rounded(.up))]
let c = geometry.isTall ? r : r.reversed()
return (x: c[0], y: c[1])
}
func grid(axis: Axis, geometry: GeometryProxy) -> some View {
ScrollView([axis == .vertical ? .vertical : .horizontal]) {
HorVGrid(axis: axis, columns: [GridItem(.flexible())], spacing: spacing) {
ForEach(0 ..< views.count, id: \.self) { i in
views[i]
.aspectRatio(1, contentMode: .fill)
}
}
.padding(axis == .horizontal ? [.leading, .trailing] : [.top, .bottom],
max(0, ((axis == .horizontal ? geometry.size.width : geometry.size.height)
- ((axis == .horizontal ? geometry.size.height : geometry.size.width) * CGFloat(views.count)) - (spacing * CGFloat(views.count - 1))) / 2))
}
}
var body: some View {
GeometryReader { geometry in
if views.isEmpty {
EmptyView()
} else if geometry.size.width <= 300 {
grid(axis: .vertical, geometry: geometry)
} else if geometry.size.height <= 300 {
grid(axis: .horizontal, geometry: geometry)
} else {
let verticalWhenTall: Axis = geometry.isTall ? .vertical : .horizontal
let horizontalWhenTall: Axis = geometry.isTall ? .horizontal : .vertical
switch views.count {
// simply return first view
case 1: views[0]
case 3: HorVStack(axis: verticalWhenTall, spacing: spacing) {
views[0]
HorVStack(axis: horizontalWhenTall, spacing: spacing) {
views[1]
views[2]
}
}
case 5: HorVStack(axis: verticalWhenTall, spacing: spacing) {
views[0]
if geometry.isTall {
HStack(spacing: spacing) {
views[1]
views[2]
}
HStack(spacing: spacing) {
views[3]
views[4]
}
} else {
VStack(spacing: spacing) {
views[1]
views[3]
}
VStack(spacing: spacing) {
views[2]
views[4]
}
}
}
default:
let c = computeColumn(with: geometry)
VStack(spacing: spacing) {
ForEach(0 ... (c.y - 1), id: \.self) { y in
HStack(spacing: spacing) {
ForEach(0 ... (c.x - 1), id: \.self) { x in
let index = (y * c.x) + x
if index < views.count {
views[index]
}
}
}
}
}
}
}
}
}
}
struct HorVStack<Content: View>: View {
let axis: Axis
let horizontalAlignment: HorizontalAlignment
let verticalAlignment: VerticalAlignment
let spacing: CGFloat?
let content: () -> Content
init(axis: Axis = .horizontal,
horizontalAlignment: HorizontalAlignment = .center,
verticalAlignment: VerticalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: @escaping () -> Content)
{
self.axis = axis
self.horizontalAlignment = horizontalAlignment
self.verticalAlignment = verticalAlignment
self.spacing = spacing
self.content = content
}
var body: some View {
Group {
if axis == .vertical {
VStack(alignment: horizontalAlignment, spacing: spacing, content: content)
} else {
HStack(alignment: verticalAlignment, spacing: spacing, content: content)
}
}
}
}
struct HorVGrid<Content: View>: View {
let axis: Axis
let spacing: CGFloat?
let content: () -> Content
let columns: [GridItem]
init(axis: Axis = .horizontal,
columns: [GridItem],
spacing: CGFloat? = nil,
@ViewBuilder content: @escaping () -> Content)
{
self.axis = axis
self.spacing = spacing
self.columns = columns
self.content = content
}
var body: some View {
Group {
if axis == .vertical {
LazyVGrid(columns: columns, spacing: spacing, content: content)
} else {
LazyHGrid(rows: columns, spacing: spacing, content: content)
}
}
}
}
extension GeometryProxy {
public var isTall: Bool {
size.height > size.width
}
var isWide: Bool {
size.width > size.height
}
}