diff --git a/LiveKitLivestreamExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LiveKitLivestreamExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved index ab6609b..50f5f22 100644 --- a/LiveKitLivestreamExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LiveKitLivestreamExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,6 +9,15 @@ "version" : "1.3.9" } }, + { + "identity" : "client-components-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/livekit/client-components-swift", + "state" : { + "revision" : "82fc74016db420393f425f54af12b5467f668e1f", + "version" : "0.0.1" + } + }, { "identity" : "jwt-kit", "kind" : "remoteSourceControl", diff --git a/LiveKitLivestreamExample.xcodeproj/project.pbxproj b/LiveKitLivestreamExample.xcodeproj/project.pbxproj index a36f3cb..39eda58 100644 --- a/LiveKitLivestreamExample.xcodeproj/project.pbxproj +++ b/LiveKitLivestreamExample.xcodeproj/project.pbxproj @@ -548,7 +548,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2024090501; + CURRENT_PROJECT_VERSION = 2024090502; DEVELOPMENT_TEAM = 76TVFCUKK7; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -592,7 +592,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2024090501; + CURRENT_PROJECT_VERSION = 2024090502; DEVELOPMENT_TEAM = 76TVFCUKK7; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; diff --git a/Shared/Contexts/RoomContext.swift b/Shared/Contexts/RoomContext.swift index 765e94e..5e3a08f 100644 --- a/Shared/Contexts/RoomContext.swift +++ b/Shared/Contexts/RoomContext.swift @@ -64,6 +64,9 @@ final class RoomContext: NSObject, ObservableObject { @Published public var enableChat: Bool = true @Published public var viewersCanRequestToJoin: Bool = true + public let localCameraTrack = LocalVideoTrack.createCameraTrack() + public var localCameraTrackPublication: LocalTrackPublication? + // Computed helpers public var isStreamOwner: Bool { room.typedMetadata.creatorIdentity == room.localParticipant.identity?.stringValue @@ -79,19 +82,14 @@ final class RoomContext: NSObject, ObservableObject { logger.info("RoomContext created") } - public func set(step: Step) { + @MainActor + public func set(step: Step) async { self.step = step - } - public func backToWelcome() { - Task { @MainActor in - self.step = .welcome - } - } - - public func backToPrepare() { - Task { @MainActor in - self.step = .streamerPrepare + if step == .streamerPrepare { + try? await localCameraTrack.start() + } else if step == .welcome { + try? await localCameraTrack.stop() } } @@ -119,19 +117,12 @@ final class RoomContext: NSObject, ObservableObject { logger.debug("Connecting to room... \(res.connectionDetails)") try await room.connect(url: res.connectionDetails.wsURL, token: res.connectionDetails.token) - Task { @MainActor in - self.step = .stream - - // Separate attempt to publish - Task { - do { - try await room.localParticipant.setCamera(enabled: true) - try await room.localParticipant.setMicrophone(enabled: true) - } catch { - logger.error("Failed to publish, error: \(error)") - } - } - } + + await set(step: .stream) + + localCameraTrackPublication = try await room.localParticipant.publish(videoTrack: localCameraTrack) + try await room.localParticipant.setMicrophone(enabled: true) + logger.info("Connected") } catch let publishError { await room.disconnect() @@ -155,9 +146,7 @@ final class RoomContext: NSObject, ObservableObject { logger.debug("Connecting to room... \(res.connectionDetails)") try await room.connect(url: res.connectionDetails.wsURL, token: res.connectionDetails.token) - Task { @MainActor in - self.step = .stream - } + await set(step: .stream) logger.info("Connected") } catch { await room.disconnect() @@ -287,8 +276,8 @@ extension RoomContext: RoomDelegate { if case .disconnected = connectionState, case .connected = oldValue { - Task { @MainActor in - self.step = .welcome + Task { + await set(step: .welcome) } logger.debug("Did disconnect") @@ -305,22 +294,37 @@ extension RoomContext: RoomDelegate { } func room(_: Room, participant: Participant, didUpdatePermissions _: ParticipantPermissions) { - if let participant = participant as? LocalParticipant, - participant.canPublish - { - // Separate attempt to publish - Task { - do { - // Ensure permissions... - guard await LiveKitSDK.ensureDeviceAccess(for: [.video, .audio]) else { - // Both .video and .audio device permissions are required... - throw LivestreamError.permissions - } + if let participant = participant as? LocalParticipant { + if participant.canPublish { + // Separate attempt to publish + Task { + do { + // Ensure permissions... + guard await LiveKitSDK.ensureDeviceAccess(for: [.video, .audio]) else { + // Both .video and .audio device permissions are required... + throw LivestreamError.permissions + } - try await participant.setCamera(enabled: true) - try await participant.setMicrophone(enabled: true) - } catch { - logger.error("Failed to publish, error: \(error)") + if localCameraTrackPublication == nil { + localCameraTrackPublication = try await participant.publish(videoTrack: localCameraTrack) + } + + try await participant.setMicrophone(enabled: true) + } catch { + logger.error("Failed to publish, error: \(error)") + } + } + } else { + Task { + do { + if let localCameraTrackPublication { + try await participant.unpublish(publication: localCameraTrackPublication) + self.localCameraTrackPublication = nil + } + try await participant.setMicrophone(enabled: false) + } catch { + logger.error("Failed to unpublish, error: \(error)") + } } } } diff --git a/Shared/StreamerPrepareView.swift b/Shared/StreamerPrepareView.swift index 299c811..3f54412 100644 --- a/Shared/StreamerPrepareView.swift +++ b/Shared/StreamerPrepareView.swift @@ -55,7 +55,9 @@ struct StreamerPrepareView: View { } StyledButton { - roomCtx.backToWelcome() + Task { + await roomCtx.set(step: .welcome) + } } label: { Text("Back") } diff --git a/Shared/ViewerPrepareView.swift b/Shared/ViewerPrepareView.swift index dc53eca..f28bbed 100644 --- a/Shared/ViewerPrepareView.swift +++ b/Shared/ViewerPrepareView.swift @@ -49,7 +49,9 @@ struct ViewerPrepareView: View { } StyledButton(isEnabled: !roomCtx.connectBusy) { - roomCtx.backToWelcome() + Task { + await roomCtx.set(step: .welcome) + } } label: { Text("Back") } diff --git a/Shared/Views/PublisherVideoView.swift b/Shared/Views/PublisherVideoView.swift index 3028d9e..b96e03f 100644 --- a/Shared/Views/PublisherVideoView.swift +++ b/Shared/Views/PublisherVideoView.swift @@ -31,21 +31,19 @@ extension Image { } struct PublisherVideoPreview: View { - var body: some View { - ZStack(alignment: .topLeading) { - LocalCameraPreview() - .frame(maxWidth: .infinity, maxHeight: .infinity) - .cornerRadius(6) - } - } -} + @EnvironmentObject var roomCtx: RoomContext + @Environment(\.liveKitUIOptions) var ui: UIOptions -struct PublisherVideoView: View { var body: some View { ZStack(alignment: .topLeading) { - LocalCameraVideoView() - .frame(maxWidth: .infinity, maxHeight: .infinity) - .cornerRadius(6) + GeometryReader { geometry in + ZStack { + ui.videoDisabledView(geometry: geometry) + SwiftUIVideoView(roomCtx.localCameraTrack, mirrorMode: .mirror) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .cornerRadius(6) } } } diff --git a/Shared/WelcomeView.swift b/Shared/WelcomeView.swift index 31e6d9f..378fe17 100644 --- a/Shared/WelcomeView.swift +++ b/Shared/WelcomeView.swift @@ -42,13 +42,17 @@ struct WelcomeView: View { Spacer() StyledButton(style: .primary) { - roomCtx.set(step: .streamerPrepare) + Task { + await roomCtx.set(step: .streamerPrepare) + } } label: { Text("Start a livestream") } StyledButton { - roomCtx.set(step: .viewerPrepare) + Task { + await roomCtx.set(step: .viewerPrepare) + } } label: { Text("Join a livestream") }