Skip to content

Commit

Permalink
Ability to join call in advance with joinAheadTimeSeconds parameter (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
martinmitrevski authored Jun 25, 2024
1 parent 33de86f commit d029ddc
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### ✅ Added
- Support for custom participant sorting in the Call object. [#438](https://github.com/GetStream/stream-video-swift/pull/438)
- Ability to join call in advance with joinAheadTimeSeconds parameter. [#446](https://github.com/GetStream/stream-video-swift/pull/446)

# [1.0.8](https://github.com/GetStream/stream-video-swift/releases/tag/1.0.8)
_June 17, 2024_
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ fileprivate func content() {
notify: false
)
}

asyncContainer {
let call = streamVideo.call(callType: "livestream", callId: callId)
let backstageRequest = BackstageSettingsRequest(
enabled: true,
joinAheadTimeSeconds: 300
)
try await call.create(
members: [.init(userId: "test")],
startsAt: Date().addingTimeInterval(500),
backstage: backstageRequest
)
try await call.join()
}

asyncContainer {
let filters: [String: RawJSON] = ["user_id": .string("jaewoong")]
Expand Down
19 changes: 13 additions & 6 deletions Sources/StreamVideo/Call.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
/// - notify: A boolean indicating whether to send notifications. Default is `false`.
/// - maxDuration: An optional integer representing the maximum duration of the call in seconds.
/// - maxParticipants: An optional integer representing the maximum number of participants allowed in the call.
/// - backstage: An optional backstage request.
/// - Returns: A `CallResponse` object representing the created call.
/// - Throws: An error if the call creation fails.
@discardableResult
Expand All @@ -228,7 +229,8 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
ring: Bool = false,
notify: Bool = false,
maxDuration: Int? = nil,
maxParticipants: Int? = nil
maxParticipants: Int? = nil,
backstage: BackstageSettingsRequest? = nil
) async throws -> CallResponse {
var membersRequest = [MemberRequest]()
memberIds?.forEach {
Expand All @@ -239,14 +241,19 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
}

var settingsOverride: CallSettingsRequest?
var limits: LimitsSettingsRequest?
if maxDuration != nil || maxParticipants != nil {
settingsOverride = CallSettingsRequest(
limits: .init(
maxDurationSeconds: maxDuration,
maxParticipants: maxParticipants
)
limits = .init(
maxDurationSeconds: maxDuration,
maxParticipants: maxParticipants
)
}

settingsOverride = CallSettingsRequest(
backstage: backstage,
limits: limits
)

let request = GetOrCreateCallRequest(
data: CallRequest(
custom: custom,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,25 @@ import Foundation

public struct BackstageSettingsRequest: Codable, JSONEncodable, Hashable {
public var enabled: Bool?
public var joinAheadTimeSeconds: Int?

public init(enabled: Bool? = nil) {
public init(enabled: Bool? = nil, joinAheadTimeSeconds: Int? = nil) {
self.enabled = enabled
self.joinAheadTimeSeconds = joinAheadTimeSeconds
}

public enum CodingKeys: String, CodingKey, CaseIterable {
case enabled
case joinAheadTimeSeconds = "join_ahead_time_seconds"
}

// Encodable protocol methods

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(enabled, forKey: .enabled)
try container.encodeIfPresent(joinAheadTimeSeconds, forKey: .joinAheadTimeSeconds)
}
}

extension BackstageSettingsRequest: Sendable {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// BackstageSettingsResponse.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech
//

import Foundation


public struct BackstageSettingsResponse: Codable, JSONEncodable, Hashable {
public var enabled: Bool
public var joinAheadTimeSeconds: Int?

public init(enabled: Bool, joinAheadTimeSeconds: Int? = nil) {
self.enabled = enabled
self.joinAheadTimeSeconds = joinAheadTimeSeconds
}

public enum CodingKeys: String, CodingKey, CaseIterable {
case enabled
case joinAheadTimeSeconds = "join_ahead_time_seconds"
}

// Encodable protocol methods

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(enabled, forKey: .enabled)
try container.encodeIfPresent(joinAheadTimeSeconds, forKey: .joinAheadTimeSeconds)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public struct CallResponse: Codable, JSONEncodable, Hashable {
/** Call ID */
public var id: String
public var ingress: CallIngressResponse
public var joinAheadTimeSeconds: Int?
public var recording: Bool
public var session: CallSessionResponse?
public var settings: CallSettingsResponse
Expand All @@ -38,7 +39,7 @@ public struct CallResponse: Codable, JSONEncodable, Hashable {
/** Date/time of the last update */
public var updatedAt: Date

public init(backstage: Bool, blockedUserIds: [String], cid: String, createdAt: Date, createdBy: UserResponse, currentSessionId: String, custom: [String: RawJSON], egress: EgressResponse, endedAt: Date? = nil, id: String, ingress: CallIngressResponse, recording: Bool, session: CallSessionResponse? = nil, settings: CallSettingsResponse, startsAt: Date? = nil, team: String? = nil, thumbnails: ThumbnailResponse? = nil, transcribing: Bool, type: String, updatedAt: Date) {
public init(backstage: Bool, blockedUserIds: [String], cid: String, createdAt: Date, createdBy: UserResponse, currentSessionId: String, custom: [String: RawJSON], egress: EgressResponse, endedAt: Date? = nil, id: String, ingress: CallIngressResponse, joinAheadTimeSeconds: Int? = nil, recording: Bool, session: CallSessionResponse? = nil, settings: CallSettingsResponse, startsAt: Date? = nil, team: String? = nil, thumbnails: ThumbnailResponse? = nil, transcribing: Bool, type: String, updatedAt: Date) {
self.backstage = backstage
self.blockedUserIds = blockedUserIds
self.cid = cid
Expand All @@ -50,6 +51,7 @@ public struct CallResponse: Codable, JSONEncodable, Hashable {
self.endedAt = endedAt
self.id = id
self.ingress = ingress
self.joinAheadTimeSeconds = joinAheadTimeSeconds
self.recording = recording
self.session = session
self.settings = settings
Expand All @@ -73,6 +75,7 @@ public struct CallResponse: Codable, JSONEncodable, Hashable {
case endedAt = "ended_at"
case id
case ingress
case joinAheadTimeSeconds = "join_ahead_time_seconds"
case recording
case session
case settings
Expand All @@ -99,6 +102,7 @@ public struct CallResponse: Codable, JSONEncodable, Hashable {
try container.encodeIfPresent(endedAt, forKey: .endedAt)
try container.encode(id, forKey: .id)
try container.encode(ingress, forKey: .ingress)
try container.encodeIfPresent(joinAheadTimeSeconds, forKey: .joinAheadTimeSeconds)
try container.encode(recording, forKey: .recording)
try container.encodeIfPresent(session, forKey: .session)
try container.encode(settings, forKey: .settings)
Expand Down
26 changes: 19 additions & 7 deletions Sources/StreamVideoSwiftUI/CallViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -324,13 +324,17 @@ open class CallViewModel: ObservableObject {
/// - ring: whether the call should ring.
/// - maxDuration: An optional integer representing the maximum duration of the call in seconds.
/// - maxParticipants: An optional integer representing the maximum number of participants allowed in the call.
/// - startsAt: An optional date when the call starts.
/// - backstage: An optional request for setting up backstage.
public func startCall(
callType: String,
callId: String,
members: [Member],
ring: Bool = false,
maxDuration: Int? = nil,
maxParticipants: Int? = nil
maxParticipants: Int? = nil,
startsAt: Date? = nil,
backstage: BackstageSettingsRequest? = nil
) {
outgoingCallMembers = members
callingState = ring ? .outgoing : .joining
Expand All @@ -342,7 +346,9 @@ open class CallViewModel: ObservableObject {
members: membersRequest,
ring: ring,
maxDuration: maxDuration,
maxParticipants: maxParticipants
maxParticipants: maxParticipants,
startsAt: startsAt,
backstage: backstage
)
} else {
let call = streamVideo.call(callType: callType, callId: callId)
Expand Down Expand Up @@ -537,7 +543,9 @@ open class CallViewModel: ObservableObject {
members: [MemberRequest],
ring: Bool = false,
maxDuration: Int? = nil,
maxParticipants: Int? = nil
maxParticipants: Int? = nil,
startsAt: Date? = nil,
backstage: BackstageSettingsRequest? = nil
) {
if enteringCallTask != nil || callingState == .inCall {
return
Expand All @@ -547,12 +555,16 @@ open class CallViewModel: ObservableObject {
log.debug("Starting call")
let call = call ?? streamVideo.call(callType: callType, callId: callId)
var settingsRequest: CallSettingsRequest?
var limits: LimitsSettingsRequest?
if maxDuration != nil || maxParticipants != nil {
settingsRequest = CallSettingsRequest(
limits: .init(maxDurationSeconds: maxDuration, maxParticipants: maxParticipants)
)
limits = .init(maxDurationSeconds: maxDuration, maxParticipants: maxParticipants)
}
let options = CreateCallOptions(members: members, settings: settingsRequest)
settingsRequest = .init(backstage: backstage, limits: limits)
let options = CreateCallOptions(
members: members,
settings: settingsRequest,
startsAt: startsAt
)
let settings = localCallSettingsChange ? callSettings : nil
try await call.join(
create: true,
Expand Down
4 changes: 4 additions & 0 deletions StreamVideo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,7 @@
8442993A29422BEA0037232A /* BackportStateObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8442993929422BEA0037232A /* BackportStateObject.swift */; };
8442993C294232360037232A /* IncomingCallView_iOS13.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8442993B294232360037232A /* IncomingCallView_iOS13.swift */; };
844299412942394C0037232A /* VideoView_iOS13.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844299402942394C0037232A /* VideoView_iOS13.swift */; };
844542F02C296AAB001D5ADF /* BackstageSettingsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844542EF2C296AAA001D5ADF /* BackstageSettingsResponse.swift */; };
8446AF912A4D84F4002AB07B /* Retries_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8446AF902A4D84F4002AB07B /* Retries_Tests.swift */; };
844ADA652AD3F1AB00769F6A /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 844ADA642AD3F1AB00769F6A /* GoogleSignInSwift */; };
844ADA672AD3F21000769F6A /* GoogleSignIn.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844ADA662AD3F21000769F6A /* GoogleSignIn.plist */; };
Expand Down Expand Up @@ -1669,6 +1670,7 @@
8442993929422BEA0037232A /* BackportStateObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackportStateObject.swift; sourceTree = "<group>"; };
8442993B294232360037232A /* IncomingCallView_iOS13.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView_iOS13.swift; sourceTree = "<group>"; };
844299402942394C0037232A /* VideoView_iOS13.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView_iOS13.swift; sourceTree = "<group>"; };
844542EF2C296AAA001D5ADF /* BackstageSettingsResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackstageSettingsResponse.swift; sourceTree = "<group>"; };
8446AF902A4D84F4002AB07B /* Retries_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Retries_Tests.swift; sourceTree = "<group>"; };
844ADA662AD3F21000769F6A /* GoogleSignIn.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = GoogleSignIn.plist; sourceTree = "<group>"; };
844ADA682AD3F78F00769F6A /* GoogleHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleHelper.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3956,6 +3958,7 @@
84DC383E29ADFCFC00946713 /* Models */ = {
isa = PBXGroup;
children = (
844542EF2C296AAA001D5ADF /* BackstageSettingsResponse.swift */,
845C09962C11AAA100F725B3 /* RejectCallRequest.swift */,
845C09822C0DEB5C00F725B3 /* LimitsSettingsRequest.swift */,
845C09832C0DEB5C00F725B3 /* LimitsSettingsResponse.swift */,
Expand Down Expand Up @@ -5155,6 +5158,7 @@
8409465B29AF4EEC007AF5BF /* ListRecordingsResponse.swift in Sources */,
8490DD21298D4ADF007E53D2 /* StreamJsonDecoder.swift in Sources */,
84C4004229E3F446007B69C2 /* ConnectedEvent.swift in Sources */,
844542F02C296AAB001D5ADF /* BackstageSettingsResponse.swift in Sources */,
84DC389C29ADFCFD00946713 /* GetOrCreateCallResponse.swift in Sources */,
4065839B2B877ADA00B4F979 /* CIImage+Sendable.swift in Sources */,
84DCA2242A3A0F0D000C3411 /* HTTPClient.swift in Sources */,
Expand Down
27 changes: 27 additions & 0 deletions StreamVideoTests/IntegrationTests/CallCRUDTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -651,4 +651,31 @@ final class CallCRUDTest: IntegrationTest {
pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin
XCTAssertNil(pin)
}

func test_joinBackstageRegularUser() async throws {
let firstUserCall = client.call(callType: .livestream, callId: randomCallId)
try await firstUserCall.create(
memberIds: [user1],
startsAt: Date().addingTimeInterval(15),
backstage: .init(enabled: true, joinAheadTimeSeconds: 10)
)

let secondUserClient = try await makeClient(for: user2)
let secondUserCall = secondUserClient.call(
callType: .livestream,
callId: firstUserCall.callId
)

try await firstUserCall.join()
try await customWait()

let error = await XCTAssertThrowsErrorAsync {
try await secondUserCall.join()
}

XCTAssertEqual((error as! APIError).statusCode, 403)

try await customWait(nanoseconds: 6_000_000_000)
try await secondUserCall.join()
}
}
27 changes: 27 additions & 0 deletions docusaurus/docs/iOS/03-guides/02-joining-creating-calls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,30 @@ Members are permanently associated with a call. It allows you to:
- Restrict the ability to join a call only to members
- Send a push notification to members when the call starts

#### Backstage setup

The backstage feature makes it easy to build a use-case where you and your co-hosts can setup your camera before going live. Only after you call call.goLive() the regular users be allowed to join the livestream.

However, you can also specify a `joinAheadTimeSeconds`, which allows regular users to join the livestream before it is live, in the specified join time before the stream starts.

Here's an example how to do that:

```swift
let call = streamVideo.call(callType: "livestream", callId: callId)
let backstageRequest = BackstageSettingsRequest(
enabled: true,
joinAheadTimeSeconds: 300
)
try await call.create(
members: [.init(userId: "test")],
startsAt: Date().addingTimeInterval(500),
backstage: backstageRequest
)
try await call.join()
```

In the code snippet above, we are creating a call that starts 500 seconds from now. We are also enabling backstage mode, with a `joinAheadTimeSeconds` of 300 seconds. That means that regular users will be able to join the call 200 seconds from now.

The following options are supported when creating a call:

| Option | Description | Default |
Expand All @@ -82,6 +106,9 @@ The following options are supported when creating a call:
| `team` | Restrict the access to this call to a specific team. | `nil` |
| `ring` | If you want the call to ring for each member. | `false` |
| `notify` | If you want the call to nofiy each member by sending push notification. | `false` |
| `maxDuration` | If you want to specify a max duration of the call, in seconds. | `nil` |
| `maxParticipants` | If you want to specify the max number of participants in the call. | `nil` |
| `backstage` | If you want to specify backstage setup for the call. | `nil` |

### Querying Members

Expand Down

0 comments on commit d029ddc

Please sign in to comment.