Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PBE-0] support livestream #584

Merged
merged 4 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions DemoApp/Sources/Components/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -513,3 +513,20 @@ extension AppEnvironment {

static var preferredVideoCodec: PreferredVideoCodec = .h264
}

extension AppEnvironment {

static var availableCallTypes: [String] = [
.development,
.default,
.audioRoom,
.livestream
]
static var preferredCallType: String?
}

extension String: Debuggable {
var title: String {
self
}
}
7 changes: 5 additions & 2 deletions DemoApp/Sources/DemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ struct DemoApp: App {
ZStack {
if userState == .loggedIn {
NavigationView {
DemoCallContainerView(callId: router.appState.deeplinkInfo.callId)
.navigationBarHidden(true)
DemoCallContainerView(
callId: router.appState.deeplinkInfo.callId,
callType: router.appState.deeplinkInfo.callType
)
.navigationBarHidden(true)
}
.navigationViewStyle(.stack)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,33 @@ struct DemoCallModifier<Factory: ViewFactory>: ViewModifier {
}

func body(content: Content) -> some View {
VideoViewOverlay(
rootView: content,
viewFactory: viewFactory,
viewModel: viewModel
)
.modifier(ThermalStateViewModifier())
contentView(content)
}

@MainActor
@ViewBuilder
private func contentView(_ rootView: Content) -> some View {
if
let call = viewModel.call,
call.callType == .livestream,
(call.currentUserHasCapability(.sendAudio) == false) || (call.currentUserHasCapability(.sendVideo) == false) {
ZStack {
rootView
LivestreamPlayer(
type: call.callType,
id: call.callId,
joinPolicy: .none,
showsLeaveCallButton: true,
onFullScreenStateChange: { [weak viewModel] in viewModel?.hideUIElements = $0 }
)
}
} else {
VideoViewOverlay(
rootView: rootView,
viewFactory: viewFactory,
viewModel: viewModel
)
.modifier(ThermalStateViewModifier())
}
}
}
47 changes: 47 additions & 0 deletions DemoApp/Sources/Views/CallTopView/DemoCallTopView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ struct DemoCallTopView: View {

HStack {
Spacer()
livestreamControlsView
HangUpIconView(viewModel: viewModel)
}
.frame(maxWidth: .infinity)
Expand All @@ -64,6 +65,52 @@ struct DemoCallTopView: View {
viewModel.call?.state.screenSharingSession != nil
&& viewModel.call?.state.isCurrentUserScreensharing == false
}

@ViewBuilder
private var livestreamControlsView: some View {
if let call = viewModel.call, call.callType == .livestream, call.currentUserHasCapability(.startBroadcastCall) {
Menu {
Button {
Task {
do {
if call.state.backstage {
try await call.goLive()
} else {
try await call.stopLive()
}
} catch {
log.error(error)
}
}
} label: {
if call.state.backstage {
Label {
Text("Start Live")
} icon: {
Image(systemName: "play.fill")
.foregroundColor(colors.accentGreen)
}
} else {
Label {
Text("Stop Live")
} icon: {
Image(systemName: "stop.fill")
.foregroundColor(colors.accentRed)
}
}
}

} label: {
CallIconView(
icon: Image(systemName: "gear"),
size: 44,
iconStyle: .transparent
)
}
} else {
EmptyView()
}
}
}

struct SharingIndicator: View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ struct DemoCallingViewModifier: ViewModifier {

private var isAnonymous: Bool { appState.currentUser == .anonymous }

private var callType: String {
!AppState.shared.deeplinkInfo.callType.isEmpty
? AppState.shared.deeplinkInfo.callType
: AppEnvironment.preferredCallType ?? .default
}

init(
text: Binding<String>,
viewModel: CallViewModel
Expand All @@ -41,13 +47,14 @@ struct DemoCallingViewModifier: ViewModifier {

// We may get in this situation when launching the app from a
// deeplink.

if deeplinkInfo.callId.isEmpty {
joinCallIfNeeded(with: self.text.wrappedValue)
joinCallIfNeeded(with: self.text.wrappedValue, callType: callType)
} else {
self.text.wrappedValue = deeplinkInfo.callId
joinCallIfNeeded(
with: self.text.wrappedValue,
callType: deeplinkInfo.callType
callType: callType
)
}
}
Expand All @@ -68,7 +75,7 @@ struct DemoCallingViewModifier: ViewModifier {
guard !isAnonymous else { return }
callKitAdapter.registerForIncomingCalls()
callKitAdapter.iconTemplateImageData = UIImage(named: "logo")?.pngData()
joinCallIfNeeded(with: text.wrappedValue)
joinCallIfNeeded(with: text.wrappedValue, callType: callType)
}
.onReceive(appState.$activeCall) { call in
viewModel.setActiveCall(call)
Expand All @@ -82,7 +89,7 @@ struct DemoCallingViewModifier: ViewModifier {
.toastView(toast: $viewModel.toast)
}

private func joinCallIfNeeded(with callId: String, callType: String = .default) {
private func joinCallIfNeeded(with callId: String, callType: String) {
guard !callId.isEmpty, viewModel.callingState == .idle else {
return
}
Expand All @@ -95,11 +102,15 @@ struct DemoCallingViewModifier: ViewModifier {
preferredVideoCodec: AppEnvironment.preferredVideoCodec.videoCodec
)
_ = await Task { @MainActor in
viewModel.update(
participantsSortComparators: callType == .livestream ? livestreamComparators : defaultComparators
)
viewModel.joinCall(callType: callType, callId: callId)
}.result
} catch {
log.error(error)
}
AppState.shared.deeplinkInfo = .empty
}
}
}
112 changes: 85 additions & 27 deletions DemoApp/Sources/Views/CallView/CallingView/SimpleCallingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import SwiftUI

struct SimpleCallingView: View {

private enum CallAction { case lobby, join, start(callId: String) }

@Injected(\.streamVideo) var streamVideo
@Injected(\.appearance) var appearance

@State var text = ""
@State private var callType: String
@State private var changeEnvironmentPromptForURL: URL?
@State private var showChangeEnvironmentPrompt: Bool = false

Expand All @@ -22,6 +25,16 @@ struct SimpleCallingView: View {
init(viewModel: CallViewModel, callId: String) {
self.viewModel = viewModel
text = callId
callType = {
guard
!AppState.shared.deeplinkInfo.callId.isEmpty,
!AppState.shared.deeplinkInfo.callType.isEmpty
else {
return AppEnvironment.preferredCallType ?? .default
}

return AppState.shared.deeplinkInfo.callType
}()
}

var body: some View {
Expand All @@ -46,15 +59,15 @@ struct SimpleCallingView: View {
.padding()

HStack {
Text("Call ID number")
Text("\(callTypeTitle) ID number")
.font(.caption)
.foregroundColor(.init(appearance.colors.textLowEmphasis))
Spacer()
}

HStack {
HStack {
TextField("Call ID", text: $text)
TextField("\(callTypeTitle) ID", text: $text)
.foregroundColor(appearance.colors.text)
.padding(.all, 12)
.disabled(isAnonymous)
Expand All @@ -81,53 +94,38 @@ struct SimpleCallingView: View {
Button {
resignFirstResponder()
Task {
await setPreferredVideoCodec(for: text)
viewModel.enterLobby(
callType: .default,
callId: text,
members: []
await performCallAction(
callType != .livestream ? .lobby : .join
)
}
} label: {
CallButtonView(
title: "Join Call",
title: "Join \(callTypeTitle)",
maxWidth: 120,
isDisabled: appState.loading || text.isEmpty
)
.disabled(appState.loading || text.isEmpty)
.minimumScaleFactor(0.7)
.lineLimit(1)
}
.disabled(appState.loading || text.isEmpty)
}

if canStartCall {
HStack {
Text("Don't have a Call ID?")
Text("Don't have a \(callTypeTitle) ID?")
.font(.caption)
.foregroundColor(
.init(
appearance.colors.textLowEmphasis
)
)
.foregroundColor(.init(appearance.colors.textLowEmphasis))
Spacer()
}
.padding(.top)

Button {
resignFirstResponder()
Task {
let callId = String.unique
await setPreferredVideoCodec(for: callId)
viewModel.startCall(
callType: .default,
callId: callId,
members: [],
ring: false,
maxDuration: AppEnvironment.callExpiration.duration
)
}
Task { await performCallAction(.start(callId: .unique)) }
} label: {
CallButtonView(
title: "Start New Call",
title: "Start New \(callTypeTitle)",
isDisabled: appState.loading
)
.disabled(appState.loading)
Expand All @@ -144,6 +142,7 @@ struct SimpleCallingView: View {
viewModel: viewModel
)
)
.onChange(of: text) { parseURLIfRequired($0) }
}

private var isAnonymous: Bool { appState.currentUser == .anonymous }
Expand All @@ -158,6 +157,12 @@ struct SimpleCallingView: View {
}

if deeplinkInfo.baseURL == AppEnvironment.baseURL {
if !Set(AppEnvironment.availableCallTypes).contains(deeplinkInfo.callType) {
AppEnvironment.availableCallTypes.append(deeplinkInfo.callType)
}
AppEnvironment.preferredCallType = deeplinkInfo.callType

callType = deeplinkInfo.callType
text = deeplinkInfo.callId
} else if let url = deeplinkInfo.url {
changeEnvironmentPromptForURL = url
Expand All @@ -170,11 +175,64 @@ struct SimpleCallingView: View {
}

private func setPreferredVideoCodec(for callId: String) async {
let call = streamVideo.call(callType: .default, callId: callId)
let call = streamVideo.call(callType: callType, callId: callId)
await call.updatePublishOptions(
preferredVideoCodec: AppEnvironment.preferredVideoCodec.videoCodec
)
}

private func parseURLIfRequired(_ text: String) {
let adapter = DeeplinkAdapter()
guard
let url = URL(string: text),
adapter.canHandle(url: url)
else {
return
}

let deeplinkInfo = adapter.handle(url: url).deeplinkInfo
guard !deeplinkInfo.callId.isEmpty else { return }

handleDeeplink(deeplinkInfo)
}

private var callTypeTitle: String {
switch callType {
case .livestream:
return "Livestream"
case .audioRoom:
return "AudioRoom"
default:
return "Call"
}
}

private func performCallAction(_ action: CallAction) async {
viewModel.update(
participantsSortComparators: callType == .livestream ? livestreamComparators : defaultComparators
)
switch action {
case .lobby:
await setPreferredVideoCodec(for: text)
viewModel.enterLobby(
callType: callType,
callId: text,
members: []
)
case .join:
await setPreferredVideoCodec(for: text)
viewModel.joinCall(callType: callType, callId: text)
case let .start(callId):
await setPreferredVideoCodec(for: callId)
viewModel.startCall(
callType: callType,
callId: callId,
members: [],
ring: false,
maxDuration: AppEnvironment.callExpiration.duration
)
}
}
}

extension URL: Identifiable {
Expand Down
Loading
Loading