跳轉到

Quickstart

This page takes you from an empty Xcode project to a one-to-one video call between two devices. When you are done, the two devices go through these steps:

  1. Each device initializes the SDK.
  2. The two devices add each other as friends.
  3. One device invites the other to a call, and the other accepts.
  4. The call is connected.
  5. Each device publishes its camera and microphone to the other.
  6. Each device shows the other's camera.

The code on this page comes from one small SwiftUI App. Each step shows only the code for that step and links to the page that covers the topic in depth. The complete App is at the end.

Before you begin

Ask the RevueSDK team for RevueSDK.xcframework and a license file. The official way to distribute the SDK is still being planned; until then, you get both files from the RevueSDK team.

  1. Drag RevueSDK.xcframework into your Xcode project.
  2. In your App target's General tab, under Frameworks, Libraries, and Embedded Content, set RevueSDK.xcframework to Embed & Sign.
  3. Add the license file to your App target. The sample App below expects it in the App bundle as revue.lic.

You need two devices to make a call.

Permissions

Your App needs all four of these. Without them, the camera, the microphone or the connection between the two devices does not work.

What Where Why
Camera Info.plist: NSCameraUsageDescription Send your camera.
Microphone Info.plist: NSMicrophoneUsageDescription Send your microphone.
Local network Info.plist: NSLocalNetworkUsageDescription Connect directly to devices on the same local network.
Multicast Entitlement: com.apple.developer.networking.multicast Send the broadcast that finds devices on the local network.
<key>NSCameraUsageDescription</key>
<string>Your camera is sent to the person you call.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Your voice is sent to the person you call.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Calls connect directly to devices on your local network.</string>

Direct connection over the local network is required, not optional: the SDK looks for the other device on the local network (localDiscoveryEnabled, on by default) by sending a broadcast. Your App needs both the local network entry and the multicast entitlement. With NSLocalNetworkUsageDescription alone and no multicast entitlement, the broadcast is not sent and local network discovery does not work.

The multicast entitlement must be approved by Apple before you can add it to your App. See Apple's multicast networking entitlement and TN3179: Understanding local network privacy for how to request it.

The audio background mode is needed only if a call has to keep going while the user switches to another App. A quickstart call does not need it.

Initialize

Start the SDK with Revue.initialize(_:) on Revue.shared. Two fields of Revue.Configuration are required: licenseData, the bytes of your license file, and bootstrapNodes, the nodes the SDK uses to join the network. Then register your delegates. Call it once: calling initialize again while the SDK is running throws RevueInitError.waitingForDestroy. See Initialization and configuration for the other fields, and Connection for every public bootstrap node.

/// Bootstrap nodes. The SDK has no built-in list: see Connection for every public node.
let quickstartBootstrapNodes: [Revue.BootstrapNode] = [
    Revue.BootstrapNode(address: "43.213.150.13", port: 33445,
                        publicKey: "9FF3EA9309646302E4CFF0C6A770490CCE1A61FCFEB04DDDC1886DDB6FF0390B",
                        tcpRelayPort: 33445, udpRelayPort: 33446),
]

final class QuickstartModel: ObservableObject, RevueDelegate, RoomDelegate {
    @Published var myAddress = ""
    @Published var friendRequest: String?
    @Published var incomingCall: String?
    @Published var friendIds: [String] = []
    @Published var remoteVideo: VideoTrack?
    @Published var error: String?

    /// licenseData and bootstrapNodes are required. Add your license file to the App bundle as revue.lic.
    /// Call it once: a second initialize while the SDK is running throws RevueInitError.waitingForDestroy.
    @MainActor
    func start() {
        do {
            let licenseURL = Bundle.main.url(forResource: "revue", withExtension: "lic")
            let config = Revue.Configuration(
                bootstrapNodes: quickstartBootstrapNodes,
                licenseData: licenseURL.flatMap { try? Data(contentsOf: $0) }
            )
            try Revue.shared.initialize(config)
            // Register after every initialize: destroy removes all delegates.
            Revue.shared.delegates.add(delegate: self)
            Room.shared.delegates.add(delegate: self)
            myAddress = Revue.shared.revueAddress
            friendIds = Revue.shared.friends.getFriendList()
        } catch {
            self.error = "\(error)"
        }
    }

Add each other as friends

You can only call friends. The simplest way for two devices to become friends:

  1. Device A shows its own address, Revue.revueAddress.
  2. On device B, the user types that address, and B sends a friend request with FriendManager.addFriend(address:message:).
  3. Device A receives the request in RevueDelegate and accepts it with FriendManager.addFriendNorequest(friendId:).

Many Apps show the address as a QR code for the other device to scan. That is your App's own choice; the SDK only gives you the address. See Friends.

/// Device B: send a friend request to the address device A shows.
@MainActor
func addFriend(address: String) {
    do {
        try Revue.shared.friends.addFriend(address: address, message: "Hi")
        friendIds = Revue.shared.friends.getFriendList()
    } catch {
        self.error = "\(error)"
    }
}

/// Device A: B's request arrives here.
func revue(_ revue: Revue, didReceiveFriendRequest friendId: String, message: String) {
    friendRequest = friendId
}

/// Device A: accept, and the two of you are friends.
@MainActor
func acceptFriendRequest(from friendId: String) {
    do {
        try Revue.shared.friends.addFriendNorequest(friendId: friendId)
    } catch FriendAddError.alreadyExists {
        // Already friends.
    } catch {
        self.error = "\(error)"
    }
    friendRequest = nil
    friendIds = Revue.shared.friends.getFriendList()
}

Invite and accept

Either device invites the other with FriendManager.sendInvitation(friendId:message:). The invited device receives the invitation in RevueDelegate and accepts it with FriendManager.acceptInvitation(friendId:). See Friends.

/// Either device: invite a friend to a call. Their device receives didReceiveInvitation.
@MainActor
func call(_ friendId: String) {
    do {
        try Revue.shared.friends.sendInvitation(friendId: friendId, message: "video")
    } catch {
        self.error = "\(error)"
    }
}

func revue(_ revue: Revue, friend friendId: String, didReceiveInvitation message: String?) {
    incomingCall = friendId
}

func revue(_ revue: Revue, friend friendId: String, didEndInvitation reason: InvitationEndReason) {
    incomingCall = nil
}

/// The invited device: accept the call.
@MainActor
func answer(_ friendId: String) {
    do {
        try Revue.shared.friends.acceptInvitation(friendId: friendId)
    } catch {
        self.error = "\(error)"
    }
    incomingCall = nil
}

Publish once connected

Once the invitation is accepted, both devices receive participantDidConnect in RoomDelegate: the friend is now a RemoteParticipant. The SDK does not send anything on its own. Turn on the camera and the microphone, then publish them to that participant. See Publishing tracks.

/// Both devices: the invitation was accepted and the friend is now a remote participant.
/// Turn on the camera and microphone, then publish them to that participant.
func room(_ room: Room, participantDidConnect participant: RemoteParticipant) {
    Task { @MainActor in
        guard let local = Room.shared.localParticipant else { return }
        local.setCamera(enabled: true)
        local.setMicrophone(enabled: true)
        do {
            try await local.publishCamera(to: participant)
            try await local.publishMicrophone(to: participant)
        } catch {
            self.error = "\(error)"
        }
    }
}

See the other side

The friend's camera arrives in RoomDelegate as a remote video track. Show it in a VideoView, wrapped for SwiftUI. The view does not keep its track, so keep the track in a property. See Rendering video.

/// The friend's camera arrives as a remote video track. Keep it in a property: the view does not keep it.
func room(_ room: Room, participant: RemoteParticipant, didSubscribeTrack publication: RemoteTrackPublication) {
    if let video = publication.track as? RemoteVideoTrack {
        remoteVideo = video
    }
}

func room(_ room: Room, participantDidDisconnect participant: RemoteParticipant) {
    remoteVideo = nil
}

A small SwiftUI wrapper creates the VideoView and hands it the track:

/// VideoView wrapped for SwiftUI.
struct QuickstartVideoView: UIViewRepresentable {
    let track: VideoTrack?

    func makeUIView(context: Context) -> VideoView {
        VideoView(frame: .zero)
    }

    func updateUIView(_ view: VideoView, context: Context) {
        if view.track !== track {
            view.track = track
        }
    }
}

The complete App

Run it on both devices: send a friend request from one, accept it on the other, then call.

import RevueSDK
import SwiftUI

// The Quickstart: a minimal SwiftUI App for a one-to-one video call between two devices.
// Run it on both devices: one adds the other as a friend, invites them to a call, and each sees the other's camera.

/// Bootstrap nodes. The SDK has no built-in list: see Connection for every public node.
let quickstartBootstrapNodes: [Revue.BootstrapNode] = [
    Revue.BootstrapNode(address: "43.213.150.13", port: 33445,
                        publicKey: "9FF3EA9309646302E4CFF0C6A770490CCE1A61FCFEB04DDDC1886DDB6FF0390B",
                        tcpRelayPort: 33445, udpRelayPort: 33446),
]

final class QuickstartModel: ObservableObject, RevueDelegate, RoomDelegate {
    @Published var myAddress = ""
    @Published var friendRequest: String?
    @Published var incomingCall: String?
    @Published var friendIds: [String] = []
    @Published var remoteVideo: VideoTrack?
    @Published var error: String?

    /// licenseData and bootstrapNodes are required. Add your license file to the App bundle as revue.lic.
    /// Call it once: a second initialize while the SDK is running throws RevueInitError.waitingForDestroy.
    @MainActor
    func start() {
        do {
            let licenseURL = Bundle.main.url(forResource: "revue", withExtension: "lic")
            let config = Revue.Configuration(
                bootstrapNodes: quickstartBootstrapNodes,
                licenseData: licenseURL.flatMap { try? Data(contentsOf: $0) }
            )
            try Revue.shared.initialize(config)
            // Register after every initialize: destroy removes all delegates.
            Revue.shared.delegates.add(delegate: self)
            Room.shared.delegates.add(delegate: self)
            myAddress = Revue.shared.revueAddress
            friendIds = Revue.shared.friends.getFriendList()
        } catch {
            self.error = "\(error)"
        }
    }

    /// Device B: send a friend request to the address device A shows.
    @MainActor
    func addFriend(address: String) {
        do {
            try Revue.shared.friends.addFriend(address: address, message: "Hi")
            friendIds = Revue.shared.friends.getFriendList()
        } catch {
            self.error = "\(error)"
        }
    }

    /// Device A: B's request arrives here.
    func revue(_ revue: Revue, didReceiveFriendRequest friendId: String, message: String) {
        friendRequest = friendId
    }

    /// Device A: accept, and the two of you are friends.
    @MainActor
    func acceptFriendRequest(from friendId: String) {
        do {
            try Revue.shared.friends.addFriendNorequest(friendId: friendId)
        } catch FriendAddError.alreadyExists {
            // Already friends.
        } catch {
            self.error = "\(error)"
        }
        friendRequest = nil
        friendIds = Revue.shared.friends.getFriendList()
    }

    /// Either device: invite a friend to a call. Their device receives didReceiveInvitation.
    @MainActor
    func call(_ friendId: String) {
        do {
            try Revue.shared.friends.sendInvitation(friendId: friendId, message: "video")
        } catch {
            self.error = "\(error)"
        }
    }

    func revue(_ revue: Revue, friend friendId: String, didReceiveInvitation message: String?) {
        incomingCall = friendId
    }

    func revue(_ revue: Revue, friend friendId: String, didEndInvitation reason: InvitationEndReason) {
        incomingCall = nil
    }

    /// The invited device: accept the call.
    @MainActor
    func answer(_ friendId: String) {
        do {
            try Revue.shared.friends.acceptInvitation(friendId: friendId)
        } catch {
            self.error = "\(error)"
        }
        incomingCall = nil
    }

    /// Both devices: the invitation was accepted and the friend is now a remote participant.
    /// Turn on the camera and microphone, then publish them to that participant.
    func room(_ room: Room, participantDidConnect participant: RemoteParticipant) {
        Task { @MainActor in
            guard let local = Room.shared.localParticipant else { return }
            local.setCamera(enabled: true)
            local.setMicrophone(enabled: true)
            do {
                try await local.publishCamera(to: participant)
                try await local.publishMicrophone(to: participant)
            } catch {
                self.error = "\(error)"
            }
        }
    }

    /// The friend's camera arrives as a remote video track. Keep it in a property: the view does not keep it.
    func room(_ room: Room, participant: RemoteParticipant, didSubscribeTrack publication: RemoteTrackPublication) {
        if let video = publication.track as? RemoteVideoTrack {
            remoteVideo = video
        }
    }

    func room(_ room: Room, participantDidDisconnect participant: RemoteParticipant) {
        remoteVideo = nil
    }
}

/// VideoView wrapped for SwiftUI.
struct QuickstartVideoView: UIViewRepresentable {
    let track: VideoTrack?

    func makeUIView(context: Context) -> VideoView {
        VideoView(frame: .zero)
    }

    func updateUIView(_ view: VideoView, context: Context) {
        if view.track !== track {
            view.track = track
        }
    }
}

struct QuickstartView: View {
    @StateObject private var model = QuickstartModel()
    @State private var friendAddress = ""

    var body: some View {
        // The ZStack stays on screen while its content switches between the call and the friend list,
        // so onAppear runs once and start() initializes the SDK once.
        ZStack {
            if let video = model.remoteVideo {
                QuickstartVideoView(track: video).ignoresSafeArea()
            } else {
                List {
                    Section("My address") {
                        Text(model.myAddress).textSelection(.enabled).font(.footnote.monospaced())
                    }
                    Section("Add a friend") {
                        TextField("Friend's address", text: $friendAddress)
                        Button("Send friend request") { model.addFriend(address: friendAddress) }
                    }
                    if let friendId = model.friendRequest {
                        Button("Accept friend request") { model.acceptFriendRequest(from: friendId) }
                    }
                    if let friendId = model.incomingCall {
                        Button("Answer call") { model.answer(friendId) }
                    }
                    Section("Friends") {
                        ForEach(model.friendIds, id: \.self) { friendId in
                            Button("Call \(friendId.prefix(8))") { model.call(friendId) }
                        }
                    }
                    if let error = model.error {
                        Text(error).foregroundColor(.red)
                    }
                }
            }
        }
        .onAppear { model.start() }
    }
}

@main
struct QuickstartApp: App {
    var body: some Scene {
        WindowGroup {
            QuickstartView()
        }
    }
}