# RevueSDK
> Peer-to-peer voice, video and screen sharing SDK for iOS (Swift). Guides and the Swift API reference.
---
# RevueSDK documentation
> Preview: the API reference is not final yet.
RevueSDK adds peer-to-peer voice, video and screen sharing to your app.
These guides explain the concepts and walk you through each feature.
---
# Quickstart
> **Writing in progress:** This topic is still being written. It will be filled in a later update.
---
# Using RevueSDK with AI
Give your AI coding assistant the RevueSDK documentation so it writes code against the real API.
## Agent skill
Download the RevueSDK skill (revuesdk-skill.zip)
The package follows the Agent Skills format: **SKILL.md** at the root, with the guides and the Swift API
reference as Markdown files under `references/`. Unzip it into a folder named `revuesdk` inside your assistant's skills folder,
so that the skill sits at `revuesdk/SKILL.md`.
## Markdown pages and llms.txt
Every page of this site, guides and API reference alike, has a Markdown version at the same path with a
`.md` extension. Two files at the site root collect them:
- llms.txt: an index of every page, with a one-line summary each.
- llms-full.txt: the English guides and the whole Swift API reference in one file.
---
# Initialization and configuration
`Revue.shared` is the entry point of the SDK. Before you can add friends, invite them or publish tracks,
start the SDK with `initialize(_:)`. When you are done, shut it down with `destroy()`.
Call both on the main thread.
## Initialize
`initialize(_:)` takes a `Revue.Configuration`. Two fields are required:
- `licenseData`: the bytes of your license file. Without it, `initialize(_:)` throws `RevueInitError`.
- `bootstrapNodes`: the nodes the SDK uses to join the network. The SDK has no built-in list; see
[Connection](connection.md) for nodes you can use.
`initialize(_:)` throws a `RevueInitError` when the license is missing, malformed, invalid or expired, or when
the configuration is invalid. Once it returns, `Room.localParticipant` on `Room.shared` is ready.
Right after `initialize(_:)` returns, add your delegates to `Revue.delegates` and `Room.delegates`, on `Revue.shared` and `Room.shared`:
=== "Swift"
```swift
import RevueSDK
import Foundation
/// Start the SDK, then register your delegates. `destroy()` removes every delegate,
/// so register them again after every `initialize(_:)`.
@MainActor
func startRevue(licenseData: Data, bootstrapNodes: [Revue.BootstrapNode],
delegate: RevueDelegate & RoomDelegate) throws {
let config = Revue.Configuration(bootstrapNodes: bootstrapNodes, licenseData: licenseData)
try Revue.shared.initialize(config)
Revue.shared.delegates.add(delegate: delegate)
Room.shared.delegates.add(delegate: delegate)
}
```
## Configuration
| Field | Default | Meaning |
| --- | --- | --- |
| `licenseData` | `nil` | License file bytes. Required. |
| `bootstrapNodes` | none | Nodes used to join the network. Required. |
| `savedataFileName` | `nil` (`revue.bin`) | File name of the identity save file, without a path. It must not contain `/` or `..`. |
| `savedataPassword` | `nil` | Password that encrypts the save file. |
| `networkOptions` | `Revue.NetworkOptions.default` | Transport switches and port range. Read only at initialize. |
| `mqttOptions` | `nil` | Presence service settings. `nil` uses the built-in service. `.deferred` starts presence later; see [Start presence after sign-in](#start-presence-after-sign-in). |
Every field is read once, by `initialize(_:)`. To change one, destroy the SDK and initialize it again.
## Start presence after sign-in
An App that has no MQTT account at launch can initialize first and start presence later. Set `mqttOptions` to
`Revue.MqttOptions.deferred`: `initialize(_:)` then does not contact the broker and sends no presence events. Calls
and friend connections work as usual. Call `startPresence(_:)` once you have the account:
=== "Swift"
```swift
import RevueSDK
import Foundation
/// Start the SDK without presence, because there is no MQTT account yet.
@MainActor
func startRevueWithoutPresence(licenseData: Data, bootstrapNodes: [Revue.BootstrapNode],
delegate: RevueDelegate & RoomDelegate) throws {
let config = Revue.Configuration(bootstrapNodes: bootstrapNodes, licenseData: licenseData,
mqttOptions: .deferred)
try Revue.shared.initialize(config)
Revue.shared.delegates.add(delegate: delegate)
Room.shared.delegates.add(delegate: delegate)
}
/// After sign-in, start presence with the account. Call it again to switch accounts.
@MainActor
func startPresenceAfterSignIn(username: String, password: String) async throws {
let options = Revue.MqttOptions(brokerUrl: "wss://broker.example.com/mqtt",
username: username, password: password)
try await Revue.shared.startPresence(options)
}
/// After sign-out, stop presence. Calls and friend connections keep working.
@MainActor
func stopPresenceAfterSignOut() async {
await Revue.shared.stopPresence()
}
```
- When `startPresence(_:)` returns without an error, the request has been sent. It does not mean presence is
connected: wait for `.connected` in `revue(_:didUpdatePresenceConnectionState:)`.
- To switch accounts, call `startPresence(_:)` again with the new settings. The old connection closes first, so
you get `.notInitialized`, then `.connected`. Calls run in the order you made them.
- To sign out, call `stopPresence()`. You get `.notInitialized` once, and calls and friend connections are not
affected.
- `deferred` applies to one `initialize(_:)`. After `destroy()`, presence stays off until you call
`startPresence(_:)` again.
- Passing `nil` uses the built-in service. `deferStart` is ignored here.
!!! warning hint-bug "Known issue"
After `stopPresence()`, friends can see this user go `.offline` and then return to `.online`, and stay there, while a friend connection between them is still up. Until this is fixed, call `destroy()` if friends must see the user as offline.
`startPresence(_:)` throws a `RevuePresenceError`:
| Error | Meaning |
| --- | --- |
| `.notInitialized` | The SDK is not initialized, or `destroy()` is running. |
| `.invalidConfiguration` | `presenceQos` is outside 0–2, or the core rejected the settings. The running connection is not affected. |
| `.startFailed(code:)` | The connection request failed and presence is stopped. |
## Destroy
`destroy()` hangs up every call, stops the SDK and releases its network instance. It is `async`: wait for it to
return before you initialize again.
By default `destroy()` keeps everything on disk. Pass a `Revue.DestroyOptions` to delete data after the SDK has
stopped:
- `deleteSavedata`: the identity save file of this session and the files that belong to it.
- `deleteRecordings`: every recording.
- `Revue.DestroyOptions.deleteAllData` sets both.
=== "Swift"
```swift
import RevueSDK
/// Shut the SDK down and delete the identity save file and every recording.
@MainActor
func signOutAndDeleteData() async {
await Revue.shared.destroy(.deleteAllData)
}
```
## Initialize again
Three rules apply every time you restart the SDK, for example after the user changes network settings:
- `destroy()` removes every delegate from `Revue.delegates` and `Room.delegates`.
Register your delegates again after every `initialize(_:)`.
- You cannot call `initialize(_:)` while the SDK is running; call `destroy()` first.
Calling it too early throws `RevueInitError`, and the running instance is left as it is.
- `localParticipant` is `nil` before `initialize(_:)` and after `destroy()`. Do not read it outside that window.
=== "Swift"
```swift
import RevueSDK
/// Apply a new configuration: destroy the running SDK, initialize it again, and register the delegates again.
@MainActor
func restartRevue(with config: Revue.Configuration, delegate: RevueDelegate & RoomDelegate) async throws {
await Revue.shared.destroy()
try Revue.shared.initialize(config)
Revue.shared.delegates.add(delegate: delegate)
Room.shared.delegates.add(delegate: delegate)
}
```
---
# Connection
RevueSDK has no server to connect to. Devices find each other on a peer-to-peer network, and a call starts
when a friend accepts your invitation. Getting there takes four steps:
1. Start the SDK with `Revue.initialize(_:)`, passing your license and the bootstrap nodes.
2. Become friends with the person you want to call.
3. One side invites, the other accepts.
4. Both sides receive `participantDidConnect`: the two of you are now connected.
There is only one room, `Room.shared`. You never create or join a room; friends who accept your invitation
show up in it.
## Start the SDK
Call `Revue.initialize(_:)` once, with a `Revue.Configuration` that has at least these two values:
- `Revue.Configuration.licenseData`: the contents of your license file. Without a valid license,
`initialize` throws a `RevueInitError`.
- `Revue.Configuration.bootstrapNodes`: the nodes the SDK contacts first to join the network. The SDK has
no built-in list, so with no nodes nobody can find you.
=== "Swift"
```swift
import RevueSDK
import Foundation
/// Start the SDK with your license file and the bootstrap nodes, then register your delegates.
@MainActor
func startRevue(revueDelegate: RevueDelegate, roomDelegate: RoomDelegate) throws {
let licenseURL = Bundle.main.url(forResource: "revue", withExtension: "lic")
let config = Revue.Configuration(
bootstrapNodes: revueBootstrapNodes,
licenseData: licenseURL.flatMap { try? Data(contentsOf: $0) }
)
// Throws RevueInitError, for example when the license is missing or has expired.
try Revue.shared.initialize(config)
// Register after every initialize: destroy removes all delegates.
Revue.shared.delegates.add(delegate: revueDelegate)
Room.shared.delegates.add(delegate: roomDelegate)
}
```
Register your delegates after `initialize`, as above. Restarting the SDK, the other configuration values, and
`localParticipant` before the SDK starts are covered in [Initialization and configuration](initialization.md).
### Bootstrap nodes
These five public nodes are ready to use. Pass all of them; the SDK uses whichever it can reach.
| Location | Address | Port | TCP relay port | UDP relay port |
| --- | --- | --- | --- | --- |
| Taiwan, Taipei | 43.213.150.13 | 33445 | 33445 | 33446 |
| Taiwan, Taipei | 43.212.93.163 | 33445 | 33445 | 33446 |
| Singapore | 18.143.211.178 | 33445 | 33445 | 33446 |
| Hong Kong | 18.163.229.39 | 33445 | 33445 | 33446 |
| Japan, Tokyo | 13.158.239.118 | 33445 | 33445 | 33446 |
`port` is always used to join the network. `tcpRelayPort` and `udpRelayPort` are optional and
default to `nil`: leave one out and the SDK does not use that node as that kind of relay.
Without any TCP relay, devices on networks that block UDP cannot connect.
With their public keys:
=== "Swift"
```swift
import RevueSDK
/// Five public bootstrap nodes. The SDK has no built-in list: pass these, or your own, in `bootstrapNodes`.
let revueBootstrapNodes: [Revue.BootstrapNode] = [
// Taiwan, Taipei
Revue.BootstrapNode(address: "43.213.150.13", port: 33445,
publicKey: "9FF3EA9309646302E4CFF0C6A770490CCE1A61FCFEB04DDDC1886DDB6FF0390B",
tcpRelayPort: 33445, udpRelayPort: 33446),
// Taiwan, Taipei
Revue.BootstrapNode(address: "43.212.93.163", port: 33445,
publicKey: "7B7E856C72078B3896F05CD7AB76CA8840D81785BDFEDCE502867F27557F3D54",
tcpRelayPort: 33445, udpRelayPort: 33446),
// Singapore
Revue.BootstrapNode(address: "18.143.211.178", port: 33445,
publicKey: "67AB39B2F5CA0EA9D1259D12BF1092175B13343CDA5F8E05E15013DA0F77FC7B",
tcpRelayPort: 33445, udpRelayPort: 33446),
// Hong Kong
Revue.BootstrapNode(address: "18.163.229.39", port: 33445,
publicKey: "8FA77DE6925C34EA7CF655B6D99C786B20EB0807AAA8E6BE00673216A8571142",
tcpRelayPort: 33445, udpRelayPort: 33446),
// Japan, Tokyo
Revue.BootstrapNode(address: "13.158.239.118", port: 33445,
publicKey: "80D1F77158FAE08F1C26F3CF83248BB9738CF4FBF242C4299B1040FB992FB40C",
tcpRelayPort: 33445, udpRelayPort: 33446),
]
```
## Add a friend
You can only invite friends. To become friends, one side sends a friend request to the other's address
(`Revue.revueAddress`, for example shown as a QR code) with `FriendManager.addFriend(address:message:)`:
=== "Swift"
```swift
import RevueSDK
/// Your own address. Show it to the person who wants to add you, for example as a QR code.
@MainActor
func myRevueAddress() -> String {
Revue.shared.revueAddress
}
/// Send a friend request to someone else's address.
@MainActor
func sendFriendRequest(to address: String) throws {
try Revue.shared.friends.addFriend(address: address, message: "Hi, it's Alice")
}
```
The other side receives the request in its `RevueDelegate` and accepts it with
`FriendManager.addFriendNorequest(friendId:)`:
=== "Swift"
```swift
import RevueSDK
/// The other side of a friend request: ask the user, then accept so the two of you become friends.
final class FriendRequestHandler: RevueDelegate {
var onFriendRequest: ((String, String) -> Void)?
func revue(_ revue: Revue, didReceiveFriendRequest friendId: String, message: String) {
onFriendRequest?(friendId, message)
}
}
/// The user tapped Accept on the friend request.
@MainActor
func acceptFriendRequest(from friendId: String) throws {
try Revue.shared.friends.addFriendNorequest(friendId: friendId)
}
```
## Invite and accept
Invite a friend with `FriendManager.sendInvitation(friendId:message:)`. `message` is free text that the SDK
passes along without reading it, for example to say whether this is a video or a voice call. If the
invitation cannot be sent, the call throws a `FriendCallError` and the friend's device does not ring.
=== "Swift"
```swift
import RevueSDK
/// Invite a friend to a call. Their device rings; the call starts when they accept.
@MainActor
func inviteToCall(friendId: String) throws {
// Throws FriendCallError when the invitation cannot be sent, for example when the friend is not reachable.
try Revue.shared.friends.sendInvitation(friendId: friendId, message: "video")
}
```
The friend's `RevueDelegate` receives the invitation. They accept with
`FriendManager.acceptInvitation(friendId:)` or reject with `FriendManager.rejectInvitation(friendId:)`.
If the invitation ends without a call (cancelled, rejected or timed out), both sides receive
`didEndInvitation` with an `InvitationEndReason`.
=== "Swift"
```swift
import RevueSDK
/// The invited side: show an incoming-call screen, then accept or reject.
final class IncomingCallHandler: RevueDelegate {
var onIncomingCall: ((String) -> Void)?
var onInvitationEnded: ((String) -> Void)?
func revue(_ revue: Revue, friend friendId: String, didReceiveInvitation message: String?) {
onIncomingCall?(friendId)
}
func revue(_ revue: Revue, friend friendId: String, didEndInvitation reason: InvitationEndReason) {
// The caller cancelled, the invitation timed out, or you rejected it: close the incoming-call screen.
onInvitationEnded?(friendId)
}
}
/// The user tapped Accept. If this throws, keep the incoming-call screen so the user can try again.
@MainActor
func acceptCall(from friendId: String) throws {
try Revue.shared.friends.acceptInvitation(friendId: friendId)
}
/// The user tapped Reject.
@MainActor
func rejectCall(from friendId: String) {
try? Revue.shared.friends.rejectInvitation(friendId: friendId)
}
```
## Connected
When the invitation is accepted, both sides receive `participantDidConnect` in their `RoomDelegate`. From
then on the friend is a `RemoteParticipant` in `Room.remoteParticipants`, and you can publish tracks to
them. Until then, the friend is not in the room at all.
=== "Swift"
```swift
import RevueSDK
/// Both sides get participantDidConnect once the invitation is accepted. The friend is now a remote participant.
final class CallStartedHandler: RoomDelegate {
var onConnected: ((RemoteParticipant) -> Void)?
func room(_ room: Room, participantDidConnect participant: RemoteParticipant) {
// participant is now in Room.shared.remoteParticipants. Publish your tracks to them from here.
onConnected?(participant)
}
}
```
To call more people, invite each of them; every friend who accepts joins the same room.
How a call ends is covered in [Disconnection](disconnection.md).
---
# Disconnection
A call with one participant ends when either side hangs up or the connection to them fails. Both sides then
receive `participantDidDisconnect`. The room itself never closes: you can invite friends again at any time.
## Leave the room
Call `Room.disconnectAll()` on `Room.shared` to hang up on every remote participant. The room itself stays,
so you can invite friends again afterwards.
=== "Swift"
```swift
import RevueSDK
/// Hang up on every remote participant. The room itself stays, so you can invite friends again afterwards.
@MainActor
func leaveRoom() {
Room.shared.disconnectAll()
}
```
To hang up on one participant only, call `RemoteParticipant.disconnect()` on them. Everyone else stays
connected.
Call `Room.disconnectAll()` and `RemoteParticipant.disconnect()` on the main thread. Both are marked
`@MainActor`, and the disconnect events are delivered before the call returns.
=== "Swift"
```swift
import RevueSDK
/// Hang up on one remote participant. Everyone else in the room stays connected.
@MainActor
func hangUp(on friendId: String) {
Room.shared.remoteParticipants[friendId]?.disconnect()
}
```
## When a participant disconnects
Your `RoomDelegate` receives `participantDidDisconnect` for every remote participant who leaves, including
the ones you hung up on yourself. By the time it arrives, the participant has already been removed from
`Room.remoteParticipants`.
To tell why, read `RemoteParticipant.lastDisconnectReason`, a `DisconnectReason`:
| Reason | Meaning |
| --- | --- |
| `DisconnectReason.leave` | The call was ended on purpose: the other side hung up, or you did. |
| `DisconnectReason.networkLost` | The call ended because the connection to the other side failed. |
=== "Swift"
```swift
import RevueSDK
/// Find out why a remote participant left, to show the right message.
final class CallEndedHandler: RoomDelegate {
var onCallEnded: ((RemoteParticipant, String) -> Void)?
func room(_ room: Room, participantDidDisconnect participant: RemoteParticipant) {
onCallEnded?(participant, callEndedText(for: participant))
}
}
func callEndedText(for participant: RemoteParticipant) -> String {
switch participant.lastDisconnectReason {
case .networkLost?:
return "Connection to \(participant.name ?? participant.identity) was lost"
case .leave?, nil:
return "Call ended"
}
}
```
The participant's own `ParticipantDelegate` receives the same reason in `didDisconnectWithReason`.
A friend connection that drops for a moment does not end the call, and you do not receive
`participantDidDisconnect` for it. See [Connection state](connection-state.md).
## Shut down the SDK
`Revue.destroy(_:)` ends every call, removes all delegates and stops the SDK, for example when the user
signs out. After that, nobody can find you or call you until you call `initialize` again.
=== "Swift"
```swift
import RevueSDK
/// Shut the whole SDK down, for example when the user signs out. Call initialize again to start over.
@MainActor
func stopRevue() async {
await Revue.shared.destroy()
}
```
What `destroy` can delete, and how to start again, is covered in
[Initialization and configuration](initialization.md).
---
# Connection state
Three different states answer three different questions:
| State | Question | Where |
| --- | --- | --- |
| Discoverable | Can other people find me on the network? | `Revue.connectionState` |
| Friend connection | Is there a working path between me and this friend? | `RemoteParticipant.paths`, `FriendManager.getFriendPaths(friendId:)` |
| Connected | Is this friend in a call with me? | `participantDidConnect` / `participantDidDisconnect` |
Keep them apart in your UI: you can be discoverable with no friend connection, and a connected participant can
have no friend connection for a while.
The SDK reconnects automatically after a network change, and there is no API to call for it.
## Discoverable
`Revue.connectionState` is a `SelfConnectionStatus` with three levels. Your `RevueDelegate` receives
`didUpdateConnectionState` whenever it changes.
| Value | Level |
| --- | --- |
| `SelfConnectionStatus.offline` | Not discoverable: you are not on the network, so nobody can find you or call you. |
| `SelfConnectionStatus.tcp` | Discoverable through a TCP relay. |
| `SelfConnectionStatus.udp` | Discoverable directly, over UDP. This is the best level. |
=== "Swift"
```swift
import RevueSDK
/// Show whether other people can find you on the network.
final class DiscoverableHandler: RevueDelegate {
var onChange: ((String) -> Void)?
func revue(_ revue: Revue, didUpdateConnectionState status: SelfConnectionStatus) {
onChange?(discoverableText(status))
}
}
/// The current value, for example when a screen first appears.
@MainActor
func currentDiscoverableText() -> String {
discoverableText(Revue.shared.connectionState)
}
func discoverableText(_ status: SelfConnectionStatus) -> String {
switch status {
case .offline: return "Not discoverable"
case .tcp: return "Discoverable through a TCP relay"
case .udp: return "Discoverable directly"
}
}
```
## Friend connection
The friend connection is the set of paths between you and one friend, a `FriendConnectionPaths`. Several can
work at once:
| Path | Meaning |
| --- | --- |
| `FriendConnectionPaths.udpDirect` | Direct UDP, the best path. |
| `FriendConnectionPaths.udpRelay` | UDP through a relay. |
| `FriendConnectionPaths.tcpRelay` | TCP through a relay. |
`FriendConnectionPaths.isOnline` is true when at least one path works. Every friend has a friend connection,
in a call or not: your `RevueDelegate` receives `didUpdatePaths` for each friend, and
`FriendManager.getFriendPaths(friendId:)` returns the current value. During a call, the same value is also
`RemoteParticipant.paths`, and your `RoomDelegate` receives `didUpdatePaths` for that participant.
=== "Swift"
```swift
import RevueSDK
/// The friend connection to one friend: which paths to them work right now.
func friendConnectionText(_ paths: FriendConnectionPaths) -> String {
if paths.contains(.udpDirect) { return "Direct" }
if paths.contains(.udpRelay) || paths.contains(.tcpRelay) { return "Relayed" }
return "No connection"
}
/// Friends list: every friend, in a call or not.
final class FriendListConnectionHandler: RevueDelegate {
var onChange: ((String, FriendConnectionPaths) -> Void)?
func revue(_ revue: Revue, friend friendId: String, didUpdatePaths paths: FriendConnectionPaths) {
onChange?(friendId, paths)
}
}
/// During a call: empty paths do not end the call. Keep the participant and the hang-up button.
final class InCallConnectionHandler: RoomDelegate {
var onConnectionLost: ((RemoteParticipant) -> Void)?
var onConnectionBack: ((RemoteParticipant) -> Void)?
func room(_ room: Room, participant: RemoteParticipant, didUpdatePaths paths: FriendConnectionPaths) {
if paths.isOnline {
onConnectionBack?(participant)
} else {
onConnectionLost?(participant)
}
}
}
```
Empty paths during a call do not end it. Show that the connection is lost, but keep the participant on screen
and keep the hang-up button working: `RemoteParticipant.disconnect()` works at any time. The call is over only
when you receive `participantDidDisconnect`.
## Connected
A friend is connected from `participantDidConnect` until `participantDidDisconnect`. See
[Connection](connection.md) and [Disconnection](disconnection.md).
## Connection quality
While a friend connection works, `Participant.connectionQuality` says how good it is: a `ConnectionQuality`
from `ConnectionQuality.excellent` to `ConnectionQuality.bad`, or `ConnectionQuality.unknown` before there
are any statistics. The SDK rates it every second from round-trip time and packet loss. Your
`RoomDelegate` receives `didUpdateConnectionQuality` only when the level changes.
=== "Swift"
```swift
import RevueSDK
/// How good the connection to a participant is while it works: signal bars next to their video.
final class ConnectionQualityHandler: RoomDelegate {
var onChange: ((Participant, Int) -> Void)?
func room(_ room: Room, participant: Participant, didUpdateConnectionQuality quality: ConnectionQuality) {
onChange?(participant, signalBars(quality))
}
}
/// Signal bars for a quality level. participant.connectionQuality holds the current value.
func signalBars(_ quality: ConnectionQuality) -> Int {
switch quality {
case .excellent: return 4
case .good: return 3
case .fair: return 2
case .poor, .bad: return 1
case .unknown: return 0
}
}
```
---
# Room
`Room` holds everyone you are in a call with: you, and every friend you are connected to.
Get the room from `Room.shared`; you cannot create a `Room` yourself. There is one room per app because
ToxCore supports only one call at a time, with one camera and one microphone.
The room exists for the whole life of the app. You do not join or leave it: friends join when they accept
an invitation (see [Connection](../connecting/connection.md)) and leave when the call ends
(see [Disconnection](../connecting/disconnection.md)).
## Participants
- `Room.localParticipant` is you. It is ready once `initialize(_:)` returns, and is nil before
`initialize(_:)` and after `destroy()` (see [Initialization and configuration](../connecting/initialization.md)).
- `Room.remoteParticipants` holds one `RemoteParticipant` for each connected friend, keyed by friend ID.
- Between `initialize(_:)` and `destroy()`, `Room.allParticipants` lists both, and `Room.participantCount` counts
them, you included.
- `Room.activeSpeakers` lists the participants who are speaking, loudest first.
=== "Swift"
```swift
import RevueSDK
/// Look up a connected friend by friend ID. Returns nil when that friend is not in the room.
func connectedParticipant(friendId: String) -> RemoteParticipant? {
Room.shared.remoteParticipants[friendId]
}
```
## Room events
Add a `RoomDelegate` to `Room.delegates` on `Room.shared` to hear about participants joining and leaving,
tracks being published and other room events. See [Events](events.md).
## Speaker
Set `Room.isSpeakerMuted` to `true` to stop playing every received audio track. Audio is still received,
so recording keeps working.
## Leave the call
`Room.disconnectAll()` hangs up on every remote participant. The room stays, so you can invite friends again.
See [Disconnection](../connecting/disconnection.md).
---
# Events
RevueSDK reports what happens through three delegate protocols. Each one has an empty default for every
method, so implement only the events you need.
| Delegate | Add it to | Tells you about |
| --- | --- | --- |
| `RevueDelegate` | `Revue.delegates` on `Revue.shared` | Friends: friend requests, invitations, each friend's connection, name and status, and whether you are discoverable |
| `RoomDelegate` | `Room.delegates` on `Room.shared` | The call: participants joining and leaving, and every participant event below |
| `ParticipantDelegate` | `Participant.delegates` on one participant | One participant: the same participant events, for that participant only |
The delegate lists hold weak references, so keep your delegates alive yourself. `destroy()` removes every
delegate: register them again after every `initialize(_:)`
(see [Initialization and configuration](../connecting/initialization.md)).
## Two delegate layers
Every participant event reaches both the participant's ParticipantDelegate and every RoomDelegate. Use
`RoomDelegate` to handle all participants in one place, and `ParticipantDelegate` to follow one participant,
for example to drive their video tile.
Because the same event arrives twice when you use both layers, listen on one layer only for anything
that notifies the user, such as the prompt for a mute request. Otherwise the prompt appears twice.
| Event | `RoomDelegate` | `ParticipantDelegate` |
| --- | --- | --- |
| A participant joined | `participantDidConnect` | — |
| A participant left | `participantDidDisconnect` | `didDisconnectWithReason` |
| Friend connection changed | `didUpdatePaths` | `didUpdatePaths` |
| Connection quality changed | `didUpdateConnectionQuality` | `didUpdateConnectionQuality` |
| Name, status message or status changed (nil when the name or status message is cleared) | `didUpdateName`, `didUpdateStatusMessage`, `didUpdateStatus` | `didUpdateName`, `didUpdateStatusMessage`, `didUpdateStatus` |
| You published or unpublished a track | `didPublishTrack`, `didUnpublishTrack` | `didPublishTrack`, `didUnpublishTrack` |
| You started or stopped receiving a track | `didSubscribeTrack`, `didUnsubscribeTrack` | `didSubscribeTrack`, `didUnsubscribeTrack` |
| A publication was muted or unmuted | `didUpdateIsMuted` | `didUpdateIsMuted` |
| Track statistics updated | `didUpdateStatistics` | `didUpdateStatistics` |
| A participant sent you a message | `didReceiveMessage` | `didReceiveMessage` |
| A participant asked you to mute or unmute | `didReceiveMuteRequest` | `didReceiveMuteRequest` |
| Speaking started or stopped | `didUpdateSpeakingParticipants` | `didUpdateIsSpeaking` |
The name, status and mute events cover you as well as remote participants. In `RoomDelegate`, the publish
events label you as `localParticipant:` instead of `participant:`. Muting your own track with
`mute()` is not a publication event: it arrives on your `TrackDelegate` instead.
=== "Swift"
```swift
import RevueSDK
/// Show the mute request prompt from one layer only. The participant's ParticipantDelegate receives
/// the same didReceiveMuteRequest, so handling it there as well would show the prompt twice.
final class MuteRequestPrompter: RoomDelegate {
var onMuteRequest: ((RemoteParticipant, Bool) -> Void)?
func room(_ room: Room, participant: RemoteParticipant, didReceiveMuteRequest isMuted: Bool) {
onMuteRequest?(participant, isMuted)
}
}
```
To follow one participant, add a `ParticipantDelegate` to that participant:
=== "Swift"
```swift
import RevueSDK
/// Follow one participant, for example to drive their video tile. The delegate list holds weak
/// references, so keep the observer alive for as long as the tile is shown.
final class ParticipantTileObserver: ParticipantDelegate {
var onSpeaking: ((Bool) -> Void)?
var onMuted: ((TrackPublication, Bool) -> Void)?
func observe(friendId: String) {
Room.shared.remoteParticipants[friendId]?.delegates.add(delegate: self)
}
func participant(_ participant: Participant, didUpdateIsSpeaking isSpeaking: Bool) {
onSpeaking?(isSpeaking)
}
func participant(_ participant: Participant, trackPublication: TrackPublication, didUpdateIsMuted isMuted: Bool) {
onMuted?(trackPublication, isMuted)
}
}
```
### Speaking
`didUpdateIsSpeaking` goes to `ParticipantDelegate` only, each time that participant starts or stops
speaking. `RoomDelegate` receives `didUpdateSpeakingParticipants` instead: the whole list of participants
who are speaking, loudest first, each time it changes. `Room.activeSpeakers` holds the same list.
=== "Swift"
```swift
import RevueSDK
/// Highlight whoever is speaking. RoomDelegate gets the whole list of speakers, loudest first,
/// each time it changes; Room.shared.activeSpeakers holds the same list.
final class ActiveSpeakerHandler: RoomDelegate {
var onSpeakersChanged: (([Participant]) -> Void)?
func room(_ room: Room, didUpdateSpeakingParticipants speakers: [Participant]) {
onSpeakersChanged?(speakers)
}
}
```
## Threads
RevueDelegate, RoomDelegate and ParticipantDelegate are always called on the main thread, so you can update
your UI directly from any delegate method. So is `VideoCapturerDelegate`, which reports a capturer's size and
state; see [Camera and capture options](../media/capture-options.md#capture-state-and-events). Move heavy work, such as decoding a large message or writing
files, to a background thread yourself, then come back to the main thread to show the result.
=== "Swift"
```swift
import RevueSDK
import Foundation
struct ChatMessage: Decodable {
let text: String
}
/// Delegate methods are called on the main thread. Decode on a background queue,
/// then come back to the main thread to update the UI.
final class ChatMessageHandler: RoomDelegate {
var onMessage: ((RemoteParticipant, ChatMessage) -> Void)?
private let decodeQueue = DispatchQueue(label: "chat.decode", qos: .userInitiated)
func room(_ room: Room, participant: RemoteParticipant, didReceiveMessage message: String) {
decodeQueue.async { [weak self] in
guard let chat = try? JSONDecoder().decode(ChatMessage.self, from: Data(message.utf8)) else { return }
DispatchQueue.main.async {
self?.onMessage?(participant, chat)
}
}
}
}
```
## Calling thread
Call RevueSDK APIs on the main thread: that is the way they are tested. `Revue` and the methods that end a
call, `RemoteParticipant.disconnect()` and `Room.disconnectAll()`, are marked `@MainActor`, so the compiler
checks those calls for you. For the rest, follow the same rule even though the compiler does not check it.
The exception is the methods that push audio or video buffers into the SDK: `BufferCapturer.capture(_:)`
(and `BufferCapturer.capture(_:timeStampNs:rotation:)`) and `AudioBufferCapturer.capture(_:)`. You can call them
from any thread. You call them for every frame or audio buffer, so do not call them on the main thread: call
them from the thread or queue that produces the buffers, such as your capture queue, so that they do not hold
up your UI. See [Your own frames and audio](../media/capture-options.md#your-own-frames-and-audio).
---
# Errors
Methods that publish tracks or change a published track throw `RoomError`. These are the cases they throw:
| Case | Thrown by | What to do |
| --- | --- | --- |
| `videoPublicationAlreadyExists` | `LocalParticipant.publish(videoTrack:to:options:)` | Each friend gets one video publication. Unpublish the current one, then publish again. |
| `audioPublicationAlreadyExists` | `LocalParticipant.publish(audioTrack:to:options:)` | Each friend gets one audio publication. Unpublish the current one, then publish again. |
| `trackNotPublished` | `LocalTrackPublication.setCodec(_:)`, `LocalTrackPublication.setMaxFps(_:)`, `LocalTrackPublication.setVp8Encoding(_:)`, `LocalTrackPublication.setH264Encoding(_:)`, `LocalTrackPublication.requestKeyframe()` | The publication was unpublished. Publish the track again to get a new one. |
| `codecNotSupported` | `LocalTrackPublication.setVp8Encoding(_:)`, `LocalTrackPublication.setH264Encoding(_:)` | The publication uses the other codec. Call `LocalTrackPublication.setCodec(_:)` to change codec and settings together. |
| `invalidParameter` | `LocalTrackPublication.setCodec(_:)`, `LocalTrackPublication.setVp8Encoding(_:)`, `LocalTrackPublication.setH264Encoding(_:)`, `LocalTrackPublication.setMaxFps(_:)` | A value is out of range: `maxBitrate` must be positive, the frame rate must be 1 to 60. The `name` and `reason` say which. |
`videoPublicationAlreadyExists` and `audioPublicationAlreadyExists` carry the friend ID, `codecNotSupported`
carries the codec the call needs, and `invalidParameter` carries the parameter `name` and the `reason`.
`initialize(_:)` throws its own error type, `RevueInitError`; see
[Initialization and configuration](../connecting/initialization.md).
## Publishing
=== "Swift"
```swift
import RevueSDK
/// Send the camera to a friend. Each friend gets one video publication, so when the
/// screen share already goes to that friend, unpublish it and publish the camera instead.
@MainActor
func publishCamera(to participant: RemoteParticipant) async throws -> LocalTrackPublication {
let local = Room.shared.localParticipant!
do {
return try await local.publish(videoTrack: local.cameraTrack, to: participant)
} catch RoomError.videoPublicationAlreadyExists {
if let screen = local.videoPublication(source: .screenShareVideo, to: participant.identity) {
local.unpublish(publication: screen)
} else if let camera = local.videoPublication(source: .camera, to: participant.identity) {
return camera
}
return try await local.publish(videoTrack: local.cameraTrack, to: participant)
}
}
```
## Changing a publication
=== "Swift"
```swift
import RevueSDK
/// Switch a video publication to the low-latency VP8 preset.
/// Returns false when the publication was unpublished in the meantime.
@MainActor
func useLowLatencyVideo(_ publication: LocalTrackPublication) throws -> Bool {
do {
try publication.setVp8Encoding(.lowLatency)
} catch RoomError.codecNotSupported {
// The publication sends H.264: change the codec instead of the VP8 settings.
try publication.setCodec(.vp8LowLatency)
} catch RoomError.trackNotPublished {
return false
}
return true
}
/// Apply a frame rate the user typed in. Returns why it was rejected, or nil when it was applied.
@MainActor
func applyFrameRate(_ publication: LocalTrackPublication, fps: Int) throws -> String? {
do {
try publication.setMaxFps(fps)
return nil
} catch RoomError.invalidParameter(let name, let reason) {
return "\(name) \(reason)"
}
}
```
---
# Logging and debugging
The SDK reports what it is doing through log entries. You decide where they go: print them, write them to a file,
or attach them to a bug report.
## Receive logs
Subscribe to `Revue.logPublisher` on `Revue.shared`. It publishes a `RevueLogEntry` for every log line, both from the SDK
itself and from its network core. Entries arrive on the main thread, so you can update UI from the subscriber
directly.
Each entry has a `level`, a `source` (the module that wrote it), a `message` and a `timestamp`, plus the
`file`, `line` and `function` that produced it when they are known.
=== "Swift"
```swift
import RevueSDK
import Combine
/// Print every SDK log at warning level or above. Entries arrive on the main thread.
@MainActor
final class LogConsole {
private var subscription: AnyCancellable?
func start() {
subscription = Revue.shared.logPublisher
.filter { $0.level >= .warning }
.sink { entry in
print("[\(entry.level.name)] [\(entry.source)] \(entry.message)")
}
}
func stop() {
subscription = nil
}
}
```
## Log levels
`RevueLogLevel` has five levels, from most to least detailed:
| Level | Raw value | Use |
| --- | --- | --- |
| `trace` | 0 | Detailed tracing, such as network packets |
| `debug` | 1 | Debugging messages |
| `info` | 2 | General information |
| `warning` | 3 | Warnings |
| `error` | 4 | Errors |
## Choose the levels
Call `setLogLevel(_:)` on `Revue.shared` with the levels you want. It takes effect at once, without restarting the
SDK, on any thread. Read the current selection from `logLevel`.
=== "Swift"
```swift
import RevueSDK
/// Choose which log levels to output. It takes effect at once.
func showWarningsAndErrors() {
Revue.shared.setLogLevel([.warning, .error])
}
/// Turn off all SDK output.
func turnOffSdkLogs() {
Revue.shared.setLogLevel([])
}
```
The set is a selection, not a minimum level: `[.warning]` does not include `.error`, and `[.debug, .error]` skips
the levels in between. An empty set turns off all SDK output.
The default is `[.warning, .error]`. The setting covers the network core and the SDK's own log lines. Levels that
are not selected are never produced, which also lowers the load on your subscriber. Your app's own `print` output
is not affected.
On iOS, a Broadcast Upload Extension that shares the screen follows the same selection, including changes made while
it is sharing. This needs an App Group; without one, the extension logs `[.warning, .error]`. Extension logs do not reach
`logPublisher`. Read them in Console.app.
## Set the levels at startup
`Revue.Configuration` has no log setting. The selection belongs to the process, so call `setLogLevel(_:)` before
`initialize(_:)` to see the logs that `initialize(_:)` writes. Neither `initialize(_:)` nor `destroy(_:)` changes it.
=== "Swift"
```swift
import RevueSDK
/// See the logs that `initialize(_:)` writes: choose the levels first. The configuration has no log setting.
@MainActor
func initializeWithDebugLogs(_ configuration: Revue.Configuration) throws {
Revue.shared.setLogLevel([.debug, .info, .warning, .error])
try Revue.shared.initialize(configuration)
}
```
---
# Participant
`Participant` holds what the local and the remote participants have in common. You never create one: the
local user is `Room.localParticipant`, and everyone you are in a call with is a `RemoteParticipant` in
`Room.remoteParticipants`, keyed by friend ID. A few properties only have a value on a `RemoteParticipant`;
see [Only on a remote participant](#only-on-a-remote-participant).
## Identity
| Property | Meaning |
| --- | --- |
| `Participant.identity` | The friend ID. It never changes and is never empty. |
| `Participant.name` | The display name, or nil when there is none. |
| `Participant.statusMessage` | The status message the user set, or nil when there is none. |
| `Participant.userStatus` | Available (`UserStatus.none`), `UserStatus.away` or `UserStatus.busy`. |
## Tracks
`Participant.trackPublications` holds every track the participant publishes, keyed by publication ID.
`Participant.videoTracks` and `Participant.audioTracks` return the same publications, filtered by kind.
Three shortcuts return the first track of one source:
| Property | Returns |
| --- | --- |
| `Participant.firstCameraVideoTrack` | The camera track. |
| `Participant.firstScreenShareVideoTrack` | The screen share track. |
| `Participant.firstAudioTrack` | The first audio track. |
!!! note "Still returned while muted"
`Participant.firstCameraVideoTrack`, `Participant.firstScreenShareVideoTrack` and
`Participant.firstAudioTrack` keep returning the track when the participant mutes it. They return nil
only when there is no such track. Check `isMuted` on the track before you show it: while it is muted,
the participant is not sending real video or sound.
To show a camera, set the track on a `VideoView` only when it is not muted, and show your own placeholder
otherwise. Run the same check again when your delegate receives `didUpdateIsMuted` for that participant
(see [Events](../core/events.md)).
=== "Swift"
```swift
import RevueSDK
/// Show a participant's camera in their tile, or the App's placeholder when the camera is muted.
/// firstCameraVideoTrack still returns the track while the participant is muted, so check isMuted.
/// Call this again from didUpdateIsMuted.
func showCamera(of friendId: String, in videoView: VideoView) -> Bool {
let camera = Room.shared.remoteParticipants[friendId]?.firstCameraVideoTrack
guard let camera, !camera.isMuted else {
videoView.track = nil
return false // show the placeholder
}
videoView.track = camera
return true
}
```
`Participant.isCameraEnabled()`, `Participant.isMicrophoneEnabled()` and `Participant.isScreenShareEnabled()`
answer the same question in one call: true when the participant has that track and it is not muted.
## Speaking
| Property | Meaning |
| --- | --- |
| `Participant.isSpeaking` | Whether the participant is speaking now. |
| `Participant.audioLevel` | The current volume, from 0.0 to 1.0. It is 0 when the participant is not speaking. |
| `Participant.lastSpokeAt` | When the participant last started speaking. It does not change while they keep talking. |
The SDK updates them every 400 ms, for the local participant too. See [Events](../core/events.md) for the
speaking events.
## Only on a remote participant
These properties are declared on `Participant`, but only a `RemoteParticipant` fills them in. On
`Room.localParticipant` they never change.
| Property | On a `RemoteParticipant` | On the `LocalParticipant` |
| --- | --- | --- |
| `Participant.joinedAt` | When the participant connected to you, set at `participantDidConnect`. | Always nil. |
| `Participant.connectionQuality` | How good your connection to that participant is, rated every second. | Always `ConnectionQuality.unknown`. |
!!! note "Only a RemoteParticipant has a value"
`Participant.joinedAt` and `Participant.connectionQuality` only have a value on a `RemoteParticipant`.
Read them from `Room.remoteParticipants`, not from `Room.localParticipant`.
=== "Swift"
```swift
import RevueSDK
import Foundation
/// How long a participant has been in the call. joinedAt only has a value on a RemoteParticipant,
/// so read it from Room.shared.remoteParticipants, not from the local participant.
func timeInCall(of friendId: String) -> TimeInterval? {
guard let joinedAt = Room.shared.remoteParticipants[friendId]?.joinedAt else { return nil }
return Date().timeIntervalSince(joinedAt)
}
```
See [Connection state](../connecting/connection-state.md#connection-quality) for the quality levels.
---
# LocalParticipant
`LocalParticipant` is the participant that represents this device in a `Room`. Get it from
`Room.localParticipant` on `Room.shared`. You use it to publish your tracks to the remote participants, to
see what you have published, and to send them messages.
## What you have published
You publish a track to one participant at a time (see [Publishing tracks](publishing.md)), so each
recipient has a `LocalTrackPublication` of its own.
`LocalParticipant.localVideoTracks` and `LocalParticipant.localAudioTracks` return those publications as
`[LocalTrackPublication]`: one for every participant you send the track to. If you publish your camera to
three participants, `localVideoTracks` holds three publications of the camera track. Video and audio are
counted separately, and a screen share counts as video.
To find the publication you send to one participant, call `LocalParticipant.videoPublication(source:to:)`
or `LocalParticipant.audioPublication(to:)` with their friend ID. Each returns `nil` when nothing of that
kind goes to them.
## Stop publishing
`LocalParticipant.unpublish(publication:)` stops one publication: one track, to one participant.
`LocalParticipant.unpublishAll()` stops every publication at once. Every participant still in the room is
told to remove your tracks, so your tiles disappear on their side. The tracks themselves are not stopped:
the camera and microphone keep their state, and you can publish them again later.
=== "Swift"
```swift
import RevueSDK
/// Stop sending every track to everyone. Each remote participant is told to remove your tracks,
/// and receives didUnsubscribeTrack. The tracks themselves stay, so you can publish them again later.
@MainActor
func stopPublishing() {
Room.shared.localParticipant.unpublishAll()
}
```
You do not need to call `unpublishAll()` when a participant leaves. Their publications are removed for you.
## Send a message
`LocalParticipant.publish(message:to:timeout:)` sends a text message to one participant in the room. They
receive it in `didReceiveMessage`. The call returns once their device confirms delivery, and throws a
`RoomMessageError` when the message is too long or cannot be delivered.
`timeout` is how long to wait for that confirmation, in seconds:
| `timeout` | Seconds waited |
| --- | --- |
| Not given | 10 |
| 5 to 30 | The value you give |
| More than 30 | 30 |
| Less than 5, including 0 and negative values | 5 |
| NaN | 10 (the default) |
A value outside 5 to 30 is not an error: the call is clamped to that range and goes ahead.
=== "Swift"
```swift
import RevueSDK
/// Send a text message to one participant in the room and wait up to 15 seconds for it to arrive.
/// The timeout can be 5 to 30 seconds; a value outside that range is clamped, not rejected.
@MainActor
func sendRoomMessage(_ text: String, to participant: RemoteParticipant) async -> Bool {
do {
try await Room.shared.localParticipant.publish(message: text, to: participant, timeout: 15)
return true
} catch RoomMessageError.messageTooLong(let size, let maxSize) {
print("Message is \(size) bytes; the limit is \(maxSize).")
return false
} catch {
print("Message was not delivered: \(error)")
return false
}
}
```
`FriendManager.sendMessage(friendId:message:timeout:)`, which sends a message to a friend outside the room,
takes `timeout` in the same range and with the same default.
## Mute: the track and each publication
Mute works at two levels, because a track you send to three participants has three publications.
| | The track | One publication |
| --- | --- | --- |
| Call | `mute()` and `unmute()` on the track; for the microphone and screen share also `set(source:enabled:)` on `LocalParticipant` | `LocalTrackPublication.mute()` and `LocalTrackPublication.unmute()` |
| Who is affected | Every participant you send the track to | Only the participant that publication goes to |
| Hardware | Not touched: the camera keeps capturing and the microphone is only silenced | Not touched: the camera and microphone keep running for everyone else |
| Reported by | `Track.isMuted` | `TrackPublication.isMuted` on that publication |
| Event | `track(_:didUpdateIsMuted:)` on your `TrackDelegate`, added to the track's `delegates` | `participant(_:trackPublication:didUpdateIsMuted:)` on your `ParticipantDelegate`, for that publication only |
For your app's camera button, switch the hardware: `set(source: .camera, enabled:)` does the same as
`setCamera(enabled:)`. Turning it off stops the camera and shows every recipient the placeholder, and turning
it on again sends the camera to all of them. Neither changes `Track.isMuted`, and neither undoes a mute: if you
muted the camera track, unmute it yourself. Use `set(source: .microphone, enabled:)` for your microphone button.
The camera track stays the same from one room to the next. When you leave the room with
`Room.disconnectAll()` while the camera track is muted, the SDK unmutes it for you: the camera does not
turn on, and your `TrackDelegate` gets `track(_:didUpdateIsMuted:)` once. So you join the next room with the camera track
unmuted, and turning the camera on with `setCamera(enabled: true)` sends the camera.
=== "Swift"
```swift
import RevueSDK
/// Turn the camera off or on for everyone you send it to. This switches the hardware: when it is off every
/// recipient gets the placeholder. Track.isMuted and each publication's isMuted stay as they were.
@MainActor
func setCameraForEveryone(on: Bool) async throws {
try await Room.shared.localParticipant.set(source: .camera, enabled: on)
}
```
Use a publication to stop sending to one participant while the others keep receiving. Find it with
`LocalParticipant.videoPublication(source:to:)` or `LocalParticipant.audioPublication(to:)`. A muted video
publication sends that participant the placeholder; a muted audio publication keeps sending, with silent
audio.
=== "Swift"
```swift
import RevueSDK
/// Stop sending your camera to one participant only. That participant gets the placeholder; everyone
/// else keeps seeing you, and the camera keeps running. Unmute the same publication to send it again.
@MainActor
func hideCamera(from participant: RemoteParticipant) async throws {
let local = Room.shared.localParticipant!
guard let publication = local.videoPublication(source: .camera, to: participant.identity) else {
return // the camera does not go to this participant
}
try await publication.mute()
}
```
The two levels do not change each other. Muting the track leaves every publication's `isMuted` as it was,
and muting a publication leaves `Track.isMuted` as it was. What a participant receives combines both: they
get the placeholder or silence when the track is muted, or their publication is muted, or both. So after
you mute the camera track, unmuting one participant's publication does not let them see you, and a
publication you muted stays muted when you turn the camera off and on again.
The same rules apply to every track you publish: the camera, the microphone, a screen share, and tracks
you fill with your own video or audio buffers.
## Screen sharing
Turn a screen share on and off with `LocalParticipant.setScreenShare(enabled:)`. It controls capturing
only; it does not publish. After turning it on, publish the track it returns to each participant with
`LocalParticipant.publish(videoTrack:to:options:)`. Each participant gets one video publication, so
unpublish the camera to that participant first.
=== "Swift"
```swift
import RevueSDK
/// Start a screen share and send it to everyone in the room in place of the camera.
/// setScreenShare only starts capturing; publishing the track to each participant is up to you.
/// Each participant gets one video publication, so the camera publication to them is removed first.
@MainActor
func startScreenShare() async throws {
let local = Room.shared.localParticipant!
guard let screen = try await local.setScreenShare(enabled: true) else { return }
for participant in Room.shared.remoteParticipants.values {
if let camera = local.videoPublication(source: .camera, to: participant.identity) {
local.unpublish(publication: camera)
}
if local.videoPublication(source: .screenShareVideo, to: participant.identity) == nil {
_ = try await local.publish(videoTrack: screen, to: participant)
}
}
}
```
When `setScreenShare(enabled: true)` returns depends on the capture mode:
- **In-app**: it returns once ReplayKit has started capturing. It throws when the user does not allow
screen recording.
- **Broadcast**: it returns the track right away and shows the system broadcast picker. Capturing starts
only when the user taps Start there, which may be later or never.
So the return of `setScreenShare(enabled: true)` does not tell you capturing has started. In both modes,
watch `LocalParticipant.isScreenSharingPublisher`: it sends true once capturing has started and false once
it has stopped, including when the user stops a broadcast from the system status bar.
=== "Swift"
```swift
import RevueSDK
import Combine
import Foundation
/// Update the screen share button from isScreenSharingPublisher. It sends true once capturing has
/// really started (in Broadcast mode, after the user taps Start), and false once it has stopped.
@MainActor
final class ScreenShareButtonModel: ObservableObject {
@Published private(set) var isSharing = false
private var subscription: AnyCancellable?
init() {
subscription = Room.shared.localParticipant.isScreenSharingPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] sharing in self?.isSharing = sharing }
}
}
```
`setScreenShare(enabled: false)` stops capturing and sends every recipient the placeholder. The
publications stay, so the participants keep your screen share tile until you unpublish it. Turning the
screen share on again sends it to the same publications.
=== "Swift"
```swift
import RevueSDK
/// Stop the screen share. Capturing stops and every recipient gets the placeholder, but the
/// publications stay: their screen share tiles remain until you unpublish them.
/// Calling setScreenShare(enabled: true) again resumes sending to the same publications.
@MainActor
func stopScreenShare() async throws {
try await Room.shared.localParticipant.setScreenShare(enabled: false)
}
```
---
# RemoteParticipant
A `RemoteParticipant` is a friend who is in the call with you. Get one from `Room.remoteParticipants`
on `Room.shared`, keyed by friend ID.
## Ask to mute or unmute
To have a participant turn their sound off or back on, call `requestMute(_:)` on them. It asks them to
mute or unmute: pass true to ask them to mute, false to ask them to unmute. It is a request, not a
command. The participant receives it as `didReceiveMuteRequest` (see [Events](../core/events.md)), and their app
decides whether to do it, for example by showing a prompt or by turning off their microphone. Nothing
changes on their side until their app acts.
`requestMute(_:)` returns true once their device has received the request. It throws when the request
cannot be delivered: `RoomMessageError.transportFailed(underlying:)` wraps the reason, such as
`FriendMessageError.friendNotReachable` or `FriendMessageError.timeout(messageId:)` when their device does
not confirm within 10 seconds. Both of these can be retried later.
=== "Swift"
```swift
import RevueSDK
/// Ask a participant to mute (or unmute, with muted: false). Returns false when the request could
/// not be delivered, for example because they are unreachable right now; try again later.
/// Whether they actually mute is decided by their app, which receives didReceiveMuteRequest.
func askToMute(friendId: String, muted: Bool) async -> Bool {
guard let participant = Room.shared.remoteParticipants[friendId] else { return false }
do {
return try await participant.requestMute(muted)
} catch {
return false
}
}
```
---
# Publishing tracks
Publishing sends one of your tracks to one connected participant. There is no server in between, so every
publish names its recipient with `to:`, and only that participant receives the track. To send your camera to
three people, publish it three times.
## Publish to a participant
`LocalParticipant.publishCamera(to:options:)` and `LocalParticipant.publishMicrophone(to:options:)` publish
your camera and microphone. For any other track, such as a buffer track, use
`LocalParticipant.publish(videoTrack:to:options:)` or `LocalParticipant.publish(audioTrack:to:options:)`.
Each publish returns a `LocalTrackPublication`, the copy of the track that goes to that participant.
=== "Swift"
```swift
import RevueSDK
/// Send your camera and microphone to one participant. Each publish goes to that participant only:
/// to send to three people, publish three times.
@MainActor
func publishCameraAndMicrophone(to participant: RemoteParticipant) async throws {
guard let local = Room.shared.localParticipant else { return }
try await local.publishCamera(to: participant)
try await local.publishMicrophone(to: participant)
}
```
Each friend gets at most one video publication and one audio publication. Publishing a second video track to
the same friend throws `RoomError.videoPublicationAlreadyExists(friendId:)`, and a second audio track
throws `RoomError.audioPublicationAlreadyExists(friendId:)`. To switch from your camera to a screen share,
unpublish the camera first; see [Errors](../core/errors.md#publishing).
## Publish to participants who connect later
The SDK does not publish anything on its own. A participant who connects after you have published to the
others receives none of your tracks until you publish to them too. Publishing to each new participant is a
required step: do it when your `RoomDelegate` receives `participantDidConnect`.
=== "Swift"
```swift
import RevueSDK
/// The SDK does not publish to a participant who connects later. Publish to each one yourself
/// in participantDidConnect, or they will not see or hear you.
final class PublishToNewParticipants: RoomDelegate {
var onPublishFailed: ((RemoteParticipant, Error) -> Void)?
func room(_ room: Room, participantDidConnect participant: RemoteParticipant) {
Task { @MainActor in
guard let local = Room.shared.localParticipant else { return }
do {
try await local.publishCamera(to: participant)
try await local.publishMicrophone(to: participant)
} catch {
onPublishFailed?(participant, error)
}
}
}
}
```
## Publishing and turning on the camera
Publishing does not turn on the camera, and turning on the camera does not publish. Do them in whichever order
suits your app:
- If you publish before the camera is on, the publication waits: nothing is sent until the first frame
arrives, and the participant sees you from that frame on.
- If the track is muted when you publish, the participant sees that your camera is off from the start, and
sees you as soon as you unmute.
---
# Tracks
A `Track` is one stream of audio or video. Your camera, microphone and screen share are local tracks
(`LocalVideoTrack` and `LocalAudioTrack`), and what a remote participant sends you arrives as remote tracks
(`RemoteVideoTrack` and `RemoteAudioTrack`). A track tells you whether its data is flowing (`Track.isMuted`),
whether it is switched on (`Track.trackState`), what it is (`Track.kind`, `Track.source`, `Track.name`)
and, for video, its size (`Track.dimensions`).
A track reaches a participant through a publication, one per recipient. What you publish to each participant
is on [Track publications](publications.md); the numbers behind a track are on [Statistics](statistics.md).
## Where tracks come from
| Local track | Where to get it |
| --- | --- |
| Camera | `LocalParticipant.cameraTrack`; the camera runs after `setCamera(enabled: true)` |
| Microphone | `LocalParticipant.microphoneTrack`; the microphone runs after `setMicrophone(enabled: true)` |
| Screen share | `LocalParticipant.screenShareTrack`, also returned by `setScreenShare(enabled: true)` |
| Your own audio or video buffers | `LocalAudioTrack.createBufferTrack(name:)` and `LocalVideoTrack.createBufferTrack(name:source:)` |
A remote participant's tracks are on the `RemoteParticipant`: `Participant.firstCameraVideoTrack`,
`Participant.firstScreenShareVideoTrack` and `Participant.firstAudioTrack` are the shortcuts, and
`Participant.videoTracks` and `Participant.audioTracks` list the publications, each with its
`TrackPublication.track`. Each participant sends you at most one video track and one audio track at a time.
`Track.sid` identifies a track on this device only. The two sides of a call do not share sids: the sid of your
camera track and the sid the receiver sees for it are different values, and the sid of a remote track repeats
from one participant to the next. Tell tracks apart by `Track.source` and `Track.name`, and do not send a sid
to the other side.
## Mute: is the data flowing
`Track.isMuted` says whether the track's data is flowing. A muted video track sends the placeholder and a muted
audio track sends silence to every participant you send the track to, including the ones you publish it to
later: when you publish a muted track to a new participant, they get the placeholder or silence from the start.
So `isMuted` always matches what the track sends, whoever receives it.
When `isMuted` flips, the track calls `track(_:didUpdateIsMuted:)` on every `TrackDelegate` in its
`Track.delegates`, once per flip, however many participants you send it to. Like every delegate in RevueSDK,
it is called on the main thread.
=== "Swift"
```swift
import RevueSDK
/// Follow the camera track's own mute state. track(_:didUpdateIsMuted:) fires once per flip,
/// however many participants you send the track to, and arrives on the main thread.
final class CameraMuteObserver: TrackDelegate {
private(set) var isCameraMuted = false
init() {
Room.shared.localParticipant.cameraTrack.delegates.add(delegate: self)
}
func track(_ track: Track, didUpdateIsMuted isMuted: Bool) {
isCameraMuted = isMuted
}
}
```
On a remote track, `isMuted` is true while the sender is not sending you real data, whether they muted the
whole track or only your publication of it: the receiving side cannot tell the two apart. Its
`TrackDelegate` gets `track(_:didUpdateIsMuted:)` the same way.
Mute has a second level: a publication, which is one participant's copy of the track. Muting a publication
affects that participant only and does not change `Track.isMuted`. The two levels, and what a participant
receives when both apply, are on the
[LocalParticipant page](../participants/local-participant.md#mute-the-track-and-each-publication).
## Mute a local track
Call `mute()` and `unmute()` on a `LocalAudioTrack` or `LocalVideoTrack`. What they do depends on the track:
- For the microphone and screen share tracks, `mute()` is the same as `set(source:enabled:)` with `false` on
`LocalParticipant`: every recipient gets silence or the placeholder. The microphone keeps running and is silenced.
- For the camera track, `mute()` stops sending its frames: every recipient gets the placeholder, and the camera
keeps running. It is separate from `setCamera(enabled:)` and `set(source: .camera, enabled:)`, which switch the
hardware and do not change `isMuted`. Unmute the track yourself; unmuting while the camera is off still sends
the placeholder.
- For a track you fill with your own buffers, `mute()` touches only that track. A muted buffer audio track
sends silence to everyone it goes to and does not touch the microphone, so you can mute your background music
while you keep talking. A muted buffer video track sends the placeholder.
You can mute before you publish: when the track is already muted and you publish it to someone later, they
receive silence or the placeholder from the start.
Both calls fire `track(_:didUpdateIsMuted:)` once and leave every publication's `isMuted` as it was.
## Hardware is switched on LocalParticipant
The camera, microphone and screen share have one switch each, and it is on `LocalParticipant`:
`LocalParticipant.setCamera(enabled:options:)`, `LocalParticipant.setMicrophone(enabled:)` and
`LocalParticipant.setScreenShare(enabled:)`. `Track.trackState` reflects that switch: `.started` once you turn
the hardware on, `.stopped` once you turn it off, and it changes as soon as you call the switch. The camera
hardware follows after a short delay.
`LocalParticipant.isCameraActive` and `LocalParticipant.isMicrophoneActive` read it for you.
After `setMicrophone(enabled: true)`, `isMicrophoneActive` is true until
you call `setMicrophone(enabled: false)`, whatever you mute, publish or unpublish in between. The camera works the same way:
muting the camera track leaves the camera running, and only `setCamera(enabled:)` or `set(source: .camera, enabled:)` turns it on or off.
=== "Swift"
```swift
import RevueSDK
/// Turn the microphone hardware on or off. isMicrophoneActive follows setMicrophone only: after
/// setMicrophone(enabled: true) it stays true until you call setMicrophone(enabled: false),
/// whatever you mute, publish or unpublish in between.
@MainActor
func setMicrophoneHardware(on: Bool) -> Bool {
let local = Room.shared.localParticipant!
_ = local.setMicrophone(enabled: on)
return local.isMicrophoneActive
}
```
There is no `start()` or `stop()` on a track. On a remote track, `trackState` means whether data is being
received; to stop handling what a participant sends you, call `RemoteTrackPublication.set(enabled:)` on their
publication instead.
## The name of a remote track
`Track.name` on a remote track tells you what the sender published:
| What the sender published | `name` on your side |
| --- | --- |
| Their camera | `camera` |
| Their microphone | `microphone` |
| Their screen share | `screen_share` |
| A track they fill with their own buffers | The name the sender gave it with `createBufferTrack`, for example `bgm` |
When the sender replaces a buffer track with another one, say they unpublish `bgm` and publish `tts`, you get a
new track whose `name` is the new one, with `didSubscribeTrack` as for any new track. Use `Track.source` and
`Track.name` together to tell a participant's background music from their voice.
=== "Swift"
```swift
import RevueSDK
/// Tell a participant's background music from their voice. A track the sender fills with their own
/// buffers carries the name they gave it ("bgm" here); their microphone track is named "microphone".
func isPlayingBackgroundMusic(friendId: String) -> Bool {
Room.shared.remoteParticipants[friendId]?.firstAudioTrack?.name == "bgm"
}
```
## Volume of a remote audio track
`RemoteAudioTrack.volume` sets how loud you hear that participant. The range is 0 to 10: 1.0 leaves the audio
unchanged (the default), 0 silences it, and values above 1.0 amplify it. A value outside the range is clamped.
Once you have the track, setting `volume` takes effect at once on the audio already flowing.
At 0 the audio keeps arriving; only its level is zero. Statistics and speaking detection are unaffected, and a
recording of the track stays continuous, with silence in that stretch. The gain is applied once, before the
audio reaches anything that listens to the track on your side, so renderers and recordings hear the same volume.
=== "Swift"
```swift
import RevueSDK
/// Set how loud you hear one participant. 0 silences them while their audio keeps arriving,
/// 1 leaves it unchanged, and values up to 10 amplify. The change takes effect at once.
func setListeningVolume(of friendId: String, to volume: Double) {
let track = Room.shared.remoteParticipants[friendId]?.firstAudioTrack as? RemoteAudioTrack
track?.volume = volume
}
```
The value lives on the track. When the call is interrupted but not ended, for example while the connection
drops and comes back, the track and its `volume` stay. A new call is a new track, and its `volume` starts at 1.0.
If the user chose a volume for a participant, keep it yourself and set it again when their track arrives in
`didSubscribeTrack`.
=== "Swift"
```swift
import RevueSDK
/// A new call gives you a new RemoteAudioTrack, and its volume starts at 1.0. Keep the volume the
/// user chose for each participant yourself, and set it again when their track arrives.
final class ListeningVolumeStore: RoomDelegate {
private var volumes: [String: Double] = [:] // friend ID → the volume the user chose
init() {
Room.shared.delegates.add(delegate: self)
}
func remember(_ volume: Double, for friendId: String) {
volumes[friendId] = volume
let track = Room.shared.remoteParticipants[friendId]?.firstAudioTrack as? RemoteAudioTrack
track?.volume = volume
}
func room(_ room: Room, participant: RemoteParticipant, didSubscribeTrack publication: RemoteTrackPublication) {
guard let track = publication.track as? RemoteAudioTrack,
let volume = volumes[participant.identity] else { return }
track.volume = volume
}
}
```
## Dimensions
`Track.dimensions` is the width and height of a video track, and nil on an audio track. `Track.aspectRatio`
is derived from it, and 1.0 while the size is unknown. On a remote video track `dimensions` is updated from the
frames you receive, so it follows the size the sender is sending you. On a local video track it is not filled yet:
!!! note hint-unsupported "Not supported yet"
`dimensions` is nil on a local video track. It is filled on remote video tracks only.
---
# Track publications
A `TrackPublication` is one participant's copy of a track. When you publish a track to a participant, you get
a `LocalTrackPublication` for that participant, and the participant gets a `RemoteTrackPublication` for it on
their side. The publication carries the track (`TrackPublication.track`), what it is (`TrackPublication.kind`,
`TrackPublication.source`, `TrackPublication.name`), whether that copy is muted (`TrackPublication.isMuted`),
and the codec and size in use (`TrackPublication.mimeType`, `TrackPublication.dimensions`).
How to publish is on [Publishing tracks](../participants/publishing.md); the track itself is on
[Tracks](tracks.md).
## One publication per recipient
A track says whether its data is flowing; a publication says whether the data goes to that participant. You
publish a track to one participant at a time, so one track has as many publications as participants you
send it to: your camera sent to three participants is one `LocalVideoTrack` and three `LocalTrackPublication`
values. `LocalParticipant.localVideoTracks` and `LocalParticipant.localAudioTracks` list them, and
`LocalParticipant.videoPublication(source:to:)` and `LocalParticipant.audioPublication(to:)` find the one
that goes to a given participant.
=== "Swift"
```swift
import RevueSDK
/// Each participant you send a track to has a publication of its own, so whether your camera reaches one
/// participant is a question about their publication, not about the track.
@MainActor
func isCameraSent(to friendId: String) -> Bool {
Room.shared.localParticipant.videoPublication(source: .camera, to: friendId) != nil
}
```
`LocalTrackPublication.mute()` and `LocalTrackPublication.unmute()` act on that copy: muting a publication
affects that participant only, who gets the placeholder or silence while everyone else keeps receiving the
track, and the camera and microphone keep running. The track's own `Track.isMuted` does not change. How the
two levels combine is on the
[LocalParticipant page](../participants/local-participant.md#mute-the-track-and-each-publication).
The publications you receive are on the `RemoteParticipant`, in `Participant.trackPublications`,
`Participant.videoTracks` and `Participant.audioTracks`. A remote participant has at most one video and one
audio publication at a time on your side, because they send you one video and one audio track at a time.
`TrackPublication.sid` identifies the publication on this device only. The two sides of a call do not share
it, like `Track.sid`, so do not send it to the other side.
## When a remote publication appears
When a participant publishes a track to you, they first send you a declaration of it and only start sending
the data once you have received it. The SDK creates the `RemoteTrackPublication` and its track together as
soon as their declaration reaches you, and calls `didSubscribeTrack` on your `RoomDelegate` and the
participant's `ParticipantDelegate` right then, not when the first data arrives. In `didSubscribeTrack` the
publication's `track` is already set, so you can hand it to a `VideoView` or read its `name` at once; the
first frames follow.
=== "Swift"
```swift
import RevueSDK
/// Follow the video a participant sends you. didSubscribeTrack fires as soon as their declaration
/// arrives, before any frame, and the publication's track is already set, so you can hand it to a view
/// at once. didUnsubscribeTrack fires when they stop sending or replace the track.
final class RemoteVideoObserver: RoomDelegate {
var onVideoTrack: ((RemoteParticipant, RemoteVideoTrack?) -> Void)?
init() {
Room.shared.delegates.add(delegate: self)
}
func room(_ room: Room, participant: RemoteParticipant, didSubscribeTrack publication: RemoteTrackPublication) {
guard let video = publication.track as? RemoteVideoTrack else { return }
onVideoTrack?(participant, video)
}
func room(_ room: Room, participant: RemoteParticipant, didUnsubscribeTrack publication: RemoteTrackPublication) {
guard publication.kind == .video else { return }
onVideoTrack?(participant, nil)
}
}
```
When the participant unpublishes the track, the publication is removed and you get `didUnsubscribeTrack`.
When they replace the track with another one of the same kind, say they unpublish their screen share and
publish their camera, you get `didUnsubscribeTrack` for the old publication and `didSubscribeTrack` for the
new one. The data you receive is only ever fed into a track that already exists: nothing is created from the
data itself.
## isSubscribed
`TrackPublication.isSubscribed` is true whenever the publication has a track. There is no server to allow
or refuse a subscription: a local publication has its track from the moment you publish, and a remote
publication has its track from the moment the declaration arrives, so `isSubscribed` is true for every
publication you can reach. Use it the way you would in code written for a server, and it holds.
## mimeType
`TrackPublication.mimeType` names the codec in use, as `video/VP8`, `video/H264` or `audio/opus`. On a
local publication it is filled when the publish succeeds and again after `setCodec(_:)`. On a remote
publication it is nil until the first frame arrives, because the codec is read from the data, and it is
updated when the sender changes codec.
| Situation | `mimeType` |
| --- | --- |
| You published video with VP8 | `video/VP8` |
| You switched that publication to H.264 with `setCodec(_:)` | `video/H264` |
| A remote video publication, before the first frame | nil |
| The first frame arrived, encoded with VP8 | `video/VP8` |
| The sender switched to H.264 | `video/H264` |
| Any audio publication | `audio/opus` |
=== "Swift"
```swift
import RevueSDK
/// Name the codec a participant's video reaches you in: "video/VP8" or "video/H264". mimeType is nil
/// until their first frame arrives, so treat nil as "not known yet", not as an error. Audio is always
/// "audio/opus".
func incomingVideoCodec(from friendId: String) -> String {
let publication = Room.shared.remoteParticipants[friendId]?.videoTracks.first
return publication?.mimeType ?? "not known yet"
}
```
## Stop handling what a participant sends
`RemoteTrackPublication.set(enabled:)` with `false` stops your side from handling that publication: the video
is no longer decoded and the audio is no longer played or passed to anything that listens to the track, so a
recording of that track breaks off. The data is still being sent and still arrives; only your handling of it
stops. `RemoteTrackPublication.isEnabled` reports the current setting, and `set(enabled: true)` resumes.
=== "Swift"
```swift
import RevueSDK
/// Stop handling the video one participant sends you, for example while their tile is off screen.
/// Their data keeps arriving; your side only stops decoding and delivering it. Pass false to resume.
@MainActor
func stopHandlingVideo(from friendId: String, stop: Bool) {
guard let participant = Room.shared.remoteParticipants[friendId],
let publication = participant.videoTracks.first as? RemoteTrackPublication else { return }
publication.set(enabled: !stop)
}
```
This is not a mute. The publication's `isMuted` does not change, the sender is not told, and nothing happens
on their side. To hear a participant at a lower level, or not at all, while their audio keeps flowing to
recordings, set `RemoteAudioTrack.volume` instead (see [Tracks](tracks.md#volume-of-a-remote-audio-track)).
To stop a participant from sending to you, there is no call on your side: they decide what they publish.
## Dimensions
`TrackPublication.dimensions` is the width and height of a video publication, and nil on an audio publication.
On a remote publication it follows `Track.dimensions` of the track you receive, so it is the size the sender
is sending you. On a local publication it is meant to be the size you send to that participant, which can
differ from one recipient to the next; it is not filled yet:
!!! note hint-unsupported "Not supported yet"
`dimensions` is nil on a local video track. It is filled on remote video tracks only.
---
# Statistics
`Track.statistics` holds the latest `TrackStatistics` of a track: what the camera or microphone captures,
what is sent or received, and how the connection to the other side is doing. It is nil until the first
collection, and it stops updating when reporting is off. The numbers are grouped in five buckets, each nil
where it does not apply:
| Bucket | What it describes |
| --- | --- |
| `TrackStatistics.videoSource` | The frames a local video track captures, before encoding: `VideoSourceStatistics.width`, `VideoSourceStatistics.height` and `VideoSourceStatistics.fps` |
| `TrackStatistics.outboundRtp` | What is sent: `OutboundStatistics.width`, `OutboundStatistics.height` and `OutboundStatistics.fps` for video, `OutboundStatistics.audioFrameRate` for audio |
| `TrackStatistics.codec` | The video codec in use, `CodecStatistics.mimeType`, as `video/VP8` or `video/H264` |
| `TrackStatistics.inboundRtp` | What arrives on a remote track: size, rate, frames received and rendered, freezes, audio rate and level |
| `TrackStatistics.iceCandidatePair` | The connection to the participant the remote track comes from |
The track itself is on [Tracks](tracks.md); which copy of a track goes to whom is on
[Track publications](publications.md).
## Turn reporting on
Statistics are off by default. Set `Room.reportStatistics` to true and the SDK collects them once a second
for every track you send and receive; set it back to false and collection stops, which also stops the work
it costs. You can flip it at any time, during a call included.
Each collection does two things: it writes `Track.statistics`, and it calls `didUpdateStatistics` on your
`RoomDelegate` and on the participant's `ParticipantDelegate`, with the participant and the publication the
numbers belong to. The event fires only when the numbers changed since the last one, and always on the main
thread. Observe `Track.statistics` when you render one track; use the event when you keep a table of them.
=== "Swift"
```swift
import RevueSDK
/// Keep the latest statistics of every publication, yours and the ones you receive. The event names the
/// publication, so one local track sent to three participants gives three entries, one per recipient, and
/// the numbers in each event belong to that publication. Reporting is off until you turn it on.
final class StatisticsObserver: RoomDelegate {
private(set) var latest: [String: TrackStatistics] = [:] // keyed by publication.sid
init() {
Room.shared.reportStatistics = true
Room.shared.delegates.add(delegate: self)
}
func room(_ room: Room, participant: Participant, trackPublication publication: TrackPublication,
didUpdateStatistics statistics: TrackStatistics) {
latest[publication.sid] = statistics
}
/// Frames per second you send in total: the sum over the publications of your video tracks.
@MainActor
var totalVideoFpsSent: Double {
Room.shared.localParticipant.videoTracks
.compactMap { latest[$0.sid]?.outboundRtp?.fps }
.reduce(0, +)
}
}
```
## Two levels: the track and each publication
A local track is sent to each participant separately, so there are two kinds of numbers on your side, and
they live in two places.
The track's own `Track.statistics` is the capture amount: `videoSource` is what the camera captures, its
real width, height and frames per second, no matter how many participants you send it to. It does not
change when you publish the track to a fourth participant or unpublish it from one, and it is the same
whether the track is muted for some of them or not.
What you send to one participant is that participant's publication: the `statistics` on the
`LocalTrackPublication` that goes to them hold the sent width, height, frames per second and codec for
that recipient. They differ from one recipient to the next; a recipient whose publication is muted gets the
placeholder picture, not your camera frames. The numbers of a publication are what `didUpdateStatistics`
gives you with that publication: one event per publication, with its own numbers.
There is no total on the track. To know how much video you send in all, add up the publications yourself;
to know how much this device uploads in all, read `bytesSent` of the connection statistics of each
participant (see [below](#connection-statistics-icecandidatepair)), which count everything sent to them.
| Bucket | Local track | Local publication | Remote track and its publication |
| --- | --- | --- | --- |
| `videoSource` | Capture size and rate | nil | nil |
| `outboundRtp` | nil | Sent to that recipient | nil |
| `codec` | nil | Codec sent to that recipient | Codec received |
| `inboundRtp` | nil | nil | What arrives from that participant |
| `iceCandidatePair` | nil | nil | The connection to that participant |
!!! warning hint-bug "Known issue"
On a local track sent to several participants, `Track.statistics` still holds the numbers of whichever recipient was written last, and the sent width, height and fps in every `didUpdateStatistics` are the total over all recipients, not that recipient's share. The `statistics` property on `LocalTrackPublication` does not exist yet. Until this is fixed, read the capture size and rate from `videoSource` only, and treat `outboundRtp` on a local track as a total.
On the receiving side there is nothing to split: a remote track is one participant's track sent to you, so
it has one publication and `Track.statistics` of the remote track is the whole picture.
## What you receive
`inboundRtp` on a remote track says how the data from that participant arrives. For video,
`InboundStatistics.width`, `InboundStatistics.height` and `InboundStatistics.fps` are the decoded size and
rate. `InboundStatistics.framesReceived` counts every frame that arrived, including the placeholder frames
of a muted publication; `InboundStatistics.framesRendered` counts the frames handed to your view, which
excludes them. When `framesReceived` keeps growing and `framesRendered` stops, the sender muted you; when
both stop, nothing is arriving. `InboundStatistics.freezeCount` and `InboundStatistics.totalFreezesDuration`
count the gaps of more than a second between frames.
For audio, `InboundStatistics.audioFrameRate` is the number of audio segments arriving per second and
`InboundStatistics.audioLevel` the raw level of the last segment. That level is a diagnostic value, not the
smoothed `Participant.audioLevel` used for speaking detection.
## Connection statistics: iceCandidatePair
`iceCandidatePair` is the connection to that participant, not a property of the track: the SDK measures it
once per participant, and the audio and video tracks from the same participant share one
`IceCandidatePairStatistics`. It is nil on your local tracks, because a local track goes to several
participants and has no single connection; read it from any track you receive from the participant you
are interested in.
| Field | Meaning |
| --- | --- |
| `IceCandidatePairStatistics.rttMs` | Round-trip time in milliseconds; -1 until the first round trip has been measured, a few seconds into the call |
| `IceCandidatePairStatistics.jitterMs`, `IceCandidatePairStatistics.packetLossRate` | Jitter in milliseconds and packet loss in percent |
| `IceCandidatePairStatistics.uploadBitrateKbps`, `IceCandidatePairStatistics.downloadBitrateKbps`, `IceCandidatePairStatistics.remoteSuggestedBitrateKbps` | Available bitrate each way, and the bitrate the other side says it can take |
| `IceCandidatePairStatistics.bytesSent`, `IceCandidatePairStatistics.bytesReceived`, `IceCandidatePairStatistics.packetsSent`, `IceCandidatePairStatistics.packetsReceived` | Totals since the connection started; take the difference between two readings for a rate |
| `IceCandidatePairStatistics.connectionType` | 0 no connection, 1 relayed over TCP, 2 relayed over UDP, 3 direct UDP |
| `IceCandidatePairStatistics.isReliable` | True when there is a connection and the round trip has been measured; before that the other fields read 0 for lack of data |
Check `isReliable` before you show the other fields, and show the round trip as unknown while `rttMs` is
-1. `Participant.connectionQuality` is the SDK's own rating of these numbers as a `ConnectionQuality`
level, updated once a second and reported only when the level changes; see
[Connection quality](../connecting/connection-state.md#connection-quality).
=== "Swift"
```swift
import RevueSDK
/// The connection to one participant, read from any track you receive from them: their audio and video
/// tracks carry the same iceCandidatePair. rttMs is -1 until the first round trip is measured, and
/// connectionType 0 means no connection, so check isReliable before showing the other numbers.
func roundTripMilliseconds(to friendId: String) -> Int32? {
guard let participant = Room.shared.remoteParticipants[friendId] else { return nil }
let publication = participant.audioTracks.first ?? participant.videoTracks.first
guard let pair = publication?.track?.statistics?.iceCandidatePair, pair.isReliable else { return nil }
return pair.rttMs
}
```
## Fields that do not apply are nil
The same `OutboundStatistics` and `InboundStatistics` serve audio and video tracks. A field that has no
meaning for the kind of track is nil, never 0, so a 0 you read is a measured zero:
| Track | Fields that are nil |
| --- | --- |
| Audio track | `width`, `height` and `fps` of `outboundRtp` and `inboundRtp`; `videoSource` and `codec` are nil too |
| Video track | `audioFrameRate` of `outboundRtp` and `inboundRtp`, and `audioLevel` of `inboundRtp` |
---
# Camera and capture options
A capturer produces the frames or audio of a local track. Each local track has one, in `LocalVideoTrack.capturer`
or `LocalAudioTrack.capturer`:
| Track | Capturer | Who starts it |
| --- | --- | --- |
| `LocalParticipant.cameraTrack` | `CameraCapturer` | `setCamera(enabled:)` |
| `LocalParticipant.microphoneTrack` | `MicrophoneCapturer` | `setMicrophone(enabled:)` |
| `LocalParticipant.screenShareTrack` | The screen share capturer; see [Screen sharing](screen-sharing.md) | `setScreenShare(enabled:)` |
| `LocalVideoTrack.createBufferTrack(name:source:)` | `BufferCapturer` | Your App, with `startCapture()` |
| `LocalAudioTrack.createBufferTrack(name:)` | `AudioBufferCapturer` | Your App, with `startCapture()` |
This page covers how capture starts and stops, the camera and its options, tracks you fill with your own
frames or audio, processing frames before they are sent, and the capture state and events. What is sent to
each participant (codec, bitrate, frame rate) is on [Publish options and encoding](encoding.md).
## Start and stop capture
Call `startCapture()` and `stopCapture()` on buffer capturers only. The camera, the microphone and the screen
share are switched on `LocalParticipant`: `setCamera(enabled:)`, `setMicrophone(enabled:)` and
`setScreenShare(enabled:)`. Those switches keep the track, its publications and the hardware in step; calling
the capturer's own methods on these tracks bypasses that.
`LocalParticipant.isCameraActive` and `LocalParticipant.isMicrophoneActive` change as soon as you call the
switch, so a button can read them directly and quick taps are safe. The camera hardware follows a short delay
after the last call and only applies the final state, both when turning on and when turning off: the device's
camera indicator can stay on for that delay, but no camera frames are sent once you switch off. If the camera
cannot start, for example without permission, `isCameraActive` goes back to false. `setCamera(enabled:)` and
`set(source: .camera, enabled:)` do not report this as an error, so watch for that change.
Capture and publishing are independent: publishing a track does not start its capturer, and starting a
capturer does not publish anything.
## The camera
Get the camera's capturer from the camera track:
```swift
let camera = Room.shared.localParticipant?.cameraTrack.capturer as? CameraCapturer
```
The camera track and its capturer exist before the camera runs, so you can read and set options before the
first `setCamera(enabled: true)`.
### Camera capture options
`CameraCaptureOptions` decides what the camera produces. The current options are in `CameraCapturer.options`.
| Field | Default | Meaning |
| --- | --- | --- |
| `dimensions` | `.h720_169` (1280×720) | The resolution you ask for. See [Requested and actual size](#requested-and-actual-size). |
| `fps` | 30 | Frames per second the camera captures. Values above 30 rarely help: when the camera delivers fewer frames, the same picture is sent again. |
| `position` | `.front` | Which way the camera faces: `.front`, `.back`, or `.unspecified` to let the SDK pick. |
| `device` | nil | A specific camera to use. When set, it wins over `position`. |
| `autoCorrectOrientation` | true | When true, the SDK follows the device as it rotates and turns the pixels upright before sending. When false, the output keeps the orientation it had when you turned it off. |
| `isMirrored` | false | Mirrors the captured picture itself, so the participant you send to, recordings and compositing all see it mirrored. To mirror only your own preview, use `VideoView.mirrorMode` instead. |
!!! note hint-unsupported "Not supported yet"
You cannot fix the camera output to an orientation of your choice yet. Setting `autoCorrectOrientation` to false keeps whichever orientation the camera had at that moment.
`CameraCaptureOptions.Dimensions` has 16:9 presets (`.h90_169` to `.h2160_169`) and 4:3 presets (`.h120_43` to
`.h1440_43`); `Dimensions(width:height:)` gives any other size.
The fields cannot be changed in place. To change the options, make a copy with `copyWith(...)` and pass it to
`CameraCapturer.set(options:)`, while the camera runs or before it starts. A new resolution or frame rate
applies without stopping the camera. `set(options:)` is `async throws -> Bool`: it returns `true` when the
running camera now uses the new options. It returns `false` when the camera is stopped, where the options are
only recorded and apply when it starts; when a camera change could not be added and the SDK went back to the
camera you had; and when a later call replaced this one before it ran. To clear a specific camera and go back
to `position`, pass `device: .some(nil)` to `copyWith`; a running camera switches to the one for `position`.
=== "Swift"
```swift
import RevueSDK
/// Ask the camera for 1080p at 15 fps. The fields of CameraCaptureOptions cannot be changed in place:
/// copy the current options with the new values and pass the copy to set(options:).
/// Returns true when the running camera now uses the new options. Returns false when the camera is off
/// (they are used when it starts), when a camera change could not be added and the old camera was put back,
/// or when a later call replaced this one.
@MainActor
func useFullHDCamera() async throws -> Bool {
guard let camera = Room.shared.localParticipant?.cameraTrack.capturer as? CameraCapturer else { return false }
return try await camera.set(options: camera.options.copyWith(dimensions: .h1080_169, fps: 15))
}
```
You can also pass options to `setCamera(enabled: true, options:)`. It applies them the same way, but does not
throw: when they cannot be applied, the camera starts with the options it had. Call `set(options:)` when you
need to know.
### Specified camera and camera in use
Two properties name a camera, and they can differ:
- `CameraCaptureOptions.device` is the camera you ask for. When it is set, the camera starts with that one; when
the SDK cannot open it, the camera starts with the camera for `position` instead. When it is nil, the SDK picks
the camera for `position`.
- `CameraCapturer.device` is the camera in use: the one actually capturing. It is set once the camera has
started and after each successful switch, and is nil when the camera is stopped. Read it to show the camera's
name, for example, even when you only set `position`.
`CameraCapturer.captureDevices()` lists the cameras you can pass as `device`, and
`CameraCapturer.canSwitchPosition()` tells whether the device has both a front and a back camera.
`CameraCapturer.captureSession` is the `AVCaptureSession` the SDK captures with. Read it only: do not add inputs
or outputs to it, because the SDK resets its input and format whenever it switches cameras or changes the
resolution.
### Switch cameras
Three methods switch cameras:
- `CameraCapturer.switchCameraPosition()` switches between the front and the back camera.
- `CameraCapturer.set(cameraPosition:)` switches to the camera facing that way. `.unspecified` takes the first
camera the SDK finds, the front one when there is one.
- `CameraCapturer.set(options:)` switches when the new options name another `device` or `position`.
All three are `async throws -> Bool`. They return once the switch has finished. The `Bool` tells whether the
camera you asked for is now capturing:
| Returns | When |
| --- | --- |
| `true` | The camera is running and the camera you asked for is now `CameraCapturer.device`. |
| `false` | The camera is stopped: the switch is only recorded and applies when the camera starts. |
| `false` | The camera could not be added and the SDK went back to the camera you had (see below). |
| `false` | A later call replaced this one before it ran. Calls made in quick succession only switch to the last one; each earlier call returns `false` and leaves `position` and `options` to the later call. |
The call returns once the hardware has finished, a short delay after the last switch. Do not wait for it to
update a button: `position` and `options` change as soon as you call, so show the user's choice straight away
and use the returned value only to report the result.
Switching by position clears `options.device`, so the SDK goes back to picking the camera by position. While
the camera is stopped, a switch only records the new position, which applies when the camera starts.
The three methods throw `CameraCapturerError` when there is no camera facing that way, or when the `device`
passed to `set(options:)` while the camera runs cannot be opened, for example because it was unplugged. The error is thrown before
anything changes, so `position` and `options` keep their values. `CameraCapturerError` has one case:
| Case | When |
| --- | --- |
| `CameraCapturerError.deviceNotAvailable` | There is no camera facing the way you asked, or it cannot be used. |
=== "Swift"
```swift
import RevueSDK
/// Switch between the front and the back camera. Returns true when the other camera is now the one
/// capturing. Returns false when the camera is off (the new direction is used when it starts), when the
/// other camera could not be added and the old one was put back, when a later switch replaced this one,
/// or when the device has no camera facing the other way (position and options keep their values).
@MainActor
func flipCamera() async -> Bool {
guard let camera = Room.shared.localParticipant?.cameraTrack.capturer as? CameraCapturer else { return false }
do {
return try await camera.switchCameraPosition()
} catch CameraCapturerError.deviceNotAvailable {
return false // for example: tell the user this device has no back camera
} catch {
return false
}
}
```
Switching to a camera that exists can still fail a moment later, when the SDK adds it to the capture session,
for example because another App is using it. The SDK then goes back to the camera you had: `position` and
`options` return to their old values on the main thread, `CameraCapturer.device` stays the old camera, no
error is thrown, and the method returns `false`.
`CaptureState.error` is used by screen share only. A camera that fails to switch or start does not move its
capturer to `error`.
### Requested and actual size
The SDK has two size types, because what you ask for and what you get can differ:
- `CameraCaptureOptions.Dimensions` is a request: the size you want the camera to produce.
- `CMVideoDimensions` is a measurement: the size of the frames actually captured, in `VideoCapturer.dimensions`
and the events below.
For example, you ask for 1080p with `.h1080_169`, but this device's front camera goes up to 720p. The camera
then sends 1280×720: `options.dimensions` stays 1920×1080, the capturer's `VideoCapturer.dimensions` reads 1280×720, and the
capturer reports `capturer(_:didDowngradeResolution:to:)` with 1920×1080 and 1280×720. The SDK picks the
closest format the camera supports and never scales the picture up.
## Your own frames and audio
To send video or audio your App produces, such as frames from a network camera, a composited picture or a
music file, create a buffer track and push buffers into its capturer:
- `LocalVideoTrack.createBufferTrack(name:source:)` gives a video track whose capturer is a `BufferCapturer`.
- `LocalAudioTrack.createBufferTrack(name:)` gives an audio track whose capturer is an `AudioBufferCapturer`.
This is the only way to bring your own source. Do not try to subclass `VideoCapturer` or implement
`AudioCapturer` yourself: `VideoCapturer` has no public initializer, and a track's capturer is set by the SDK
when it creates the track, so a capturer of your own cannot be attached to one.
The video track's `name` defaults to `"buffer"` and its `source` to `.unknown`; the audio track's name defaults
to `"audio_buffer"`, with source `.unknown`. Give the video track `.camera` as its source when the receiver
should treat it as your camera, and give each track a name the receiver can tell apart; see
[Tracks](../tracks/tracks.md). Statistics for buffer tracks are switched on with all the others by
`Room.reportStatistics` on `Room.shared`; `createBufferTrack` has no option for them.
### Start the capturer yourself
A buffer capturer starts idle and drops every buffer you push until you call `startCapture()`. Publishing the
track does not start it, so a track you publish without starting it sends nothing, and no error tells you so.
Call `startCapture()` before you push the first buffer, also when you only want a local preview, and
`stopCapture()` when you are done. A buffer capturer released while still capturing logs an error.
### Push frames
`BufferCapturer.capture(_:)` takes a `CVPixelBuffer` in NV12 (`kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`
or `kCVPixelFormatType_420YpCbCr8BiPlanarFullRange`) or BGRA (`kCVPixelFormatType_32BGRA`). The SDK converts BGRA to NV12 itself; a frame in any
other format is dropped with a warning in the log. The frame is sent at the size you give it: to change the
resolution, change the size of the buffers you push.
`BufferCapturer.capture(_:timeStampNs:rotation:)` also takes a rotation, a `VideoRotation`: how far to turn the
frame clockwise to make it upright. The SDK turns the pixels before sending, so `._90` and `._270` swap width and
height (a 1280×720 frame is sent as 720×1280). Turning happens before the processor (see
[Process frames](#process-frames)), which receives the upright frame.
`timeStampNs` is not used for now: each frame is timed when the SDK sends it.
!!! note hint-unsupported "Not supported yet"
The `timeStampNs` you pass is ignored: each frame is timed when the SDK sends it. Frames that reach you late, such as from a network camera or after slow processing, can drift out of step with the audio.
Call `BufferCapturer.capture(_:)` and `AudioBufferCapturer.capture(_:)` from the thread or queue that produces
the buffers, not from the main thread. They are the exception to the main-thread rule; see
[Events](../core/events.md#calling-thread).
=== "Swift"
```swift
import RevueSDK
import CoreVideo
import Foundation
/// Send frames that arrive from a network camera lying on its side.
/// Start the capturer before pushing frames: until then every frame is dropped,
/// and publishing the track does not start it.
final class NetworkCameraFeed {
let track = LocalVideoTrack.createBufferTrack(name: "ipcam", source: .camera)
private let frameQueue = DispatchQueue(label: "ipcam.frames", qos: .userInitiated)
func start() async throws {
try await track.capturer?.startCapture()
}
/// Called by the network camera for every decoded frame.
func didDecode(_ frame: CVPixelBuffer) {
guard let capturer = track.capturer as? BufferCapturer else { return }
// capture is meant to be called off the main thread, from the queue that produces the frames.
frameQueue.async {
// The camera is mounted on its side: turn each frame 90° clockwise to make it upright.
capturer.capture(frame, timeStampNs: 0, rotation: ._90)
}
}
func stop() async throws {
try await track.capturer?.stopCapture()
}
}
```
`AudioBufferCapturer.capture(_:)` takes an `AVAudioPCMBuffer` and plays the same role for audio: it sends the
buffer to every participant you published the track to, and to the track's local audio renderers.
## Process frames
Set `LocalVideoTrack.processor` to a `VideoProcessor` to change each frame before it is used, for example to blur
the background or draw a watermark. It works on every local video track: the camera, the screen share and
buffer tracks.
- The SDK calls `process(pixelBuffer:)` once per frame on the capture thread, not on the main thread. A slow
processor lowers the frame rate.
- Return the frame to use, a new buffer or the same one; return nil to drop the frame.
- Your local preview and every participant you send to get the processed frame, and `VideoCapturer.dimensions`
measures it.
- The placeholder sent while the camera is off or muted does not go through the processor.
=== "Swift"
```swift
import RevueSDK
import CoreImage
import CoreVideo
/// Blur every camera frame before it is shown and sent. process(pixelBuffer:) runs on the
/// capture thread once per frame, so keep it fast; return nil to drop a frame.
final class BlurProcessor: VideoProcessor {
private let context = CIContext()
func process(pixelBuffer: CVPixelBuffer) -> CVPixelBuffer? {
let image = CIImage(cvPixelBuffer: pixelBuffer)
let blurred = image.clampedToExtent().applyingGaussianBlur(sigma: 8).cropped(to: image.extent)
context.render(blurred, to: pixelBuffer)
return pixelBuffer
}
}
@MainActor
func blurMyCamera(_ on: Bool) {
Room.shared.localParticipant?.cameraTrack.processor = on ? BlurProcessor() : nil
}
```
## Capture state and events
`VideoCapturer.captureState` and `AudioCapturer.captureState` are a `CaptureState`:
| State | Meaning |
| --- | --- |
| `idle` | Not capturing. Every capturer starts here. |
| `capturing` | Capturing. |
| `error` | Screen share could not start, for example because the user declined. Only the screen share capturer uses it. |
A video capturer tells its `VideoCapturerDelegate`s, added to `VideoCapturer.delegates`, about three changes:
| Event | When |
| --- | --- |
| `capturer(_:didUpdate:)` with a `CMVideoDimensions?` | The size of the frames changed. nil when capture stops. |
| `capturer(_:didUpdate:)` with a `CaptureState` | The capture state changed. |
| `capturer(_:didDowngradeResolution:to:)` | The camera cannot produce the size you asked for and uses a smaller one. |
The size event fires only when the size changes. The downgrade event fires every time the camera picks a format
it has to settle for (starting, switching cameras, new options), even when the size it sends does not change,
for example when you raise the request from 1080p to 4K on a camera that already sends its 1080p maximum.
Like every delegate in RevueSDK, `VideoCapturerDelegate` is called on the main thread, even when the change
comes from a capture thread or from a buffer you pushed on a background queue, so you can update your UI
directly. Each event carries the value at the moment of the change. The delegates are held weakly.
=== "Swift"
```swift
import RevueSDK
import CoreMedia
/// Show the size the camera actually sends, and tell the user when the camera cannot reach the
/// size they asked for. Both events arrive on the main thread.
final class CameraSizeObserver: VideoCapturerDelegate {
var onSize: ((CMVideoDimensions?) -> Void)?
var onDowngrade: ((CMVideoDimensions, CMVideoDimensions) -> Void)?
@MainActor
func observe() {
Room.shared.localParticipant?.cameraTrack.capturer?.delegates.add(delegate: self)
}
func capturer(_ capturer: VideoCapturer, didUpdate dimensions: CMVideoDimensions?) {
onSize?(dimensions) // nil once the camera stops
}
func capturer(_ capturer: VideoCapturer,
didDowngradeResolution requested: CMVideoDimensions,
to actual: CMVideoDimensions) {
onDowngrade?(requested, actual)
}
}
```
---
# Publish options and encoding
When you publish a track you can pass options that decide how it is encoded for that participant: the codec,
the bitrate and the frame rate. Every publish goes to one participant, so every `LocalTrackPublication` has its
own settings. You can send the same camera as VP8 to one participant and as H.264 to another, and change one
participant's settings during the call without touching the others.
Capture settings are separate. `CameraCaptureOptions` decides what the camera produces; the options on this page
decide what is sent. See [Camera and capture options](capture-options.md).
## Defaults
Leaving out `options:` is the same as passing `VideoPublishOptions()`: the codec is `VideoCodec.vp8AllKeyframe`
and `maxFps` is 15. Every frame is a keyframe, so a lost packet never leaves a broken picture on the other side,
at the cost of more bandwidth. For audio, leaving out `options:` is the same as passing
`AudioPublishOptions()`.
A buffer track from `LocalVideoTrack.createBufferTrack(name:source:)` has its own defaults: `name` is
`"buffer"` and `source` is `.unknown`, not a screen share. `createBufferTrack` has no statistics parameter;
statistics for buffer tracks are switched on with all the others by `Room.reportStatistics` on `Room.shared`.
See [Your own frames and audio](capture-options.md#your-own-frames-and-audio).
=== "Swift"
```swift
import RevueSDK
/// Send the camera with the default options: VP8 with every frame a keyframe, at most 15 fps.
@MainActor
func publishCameraWithDefaults(to participant: RemoteParticipant) async throws {
guard let local = Room.shared.localParticipant else { return }
try await local.publishCamera(to: participant)
}
/// Send the camera as H.264 at 2 Mbps and up to 24 fps.
@MainActor
func publishCameraAsH264(to participant: RemoteParticipant) async throws {
guard let local = Room.shared.localParticipant else { return }
let options = VideoPublishOptions(codec: .h264(H264Encoding(maxBitrate: 2_000_000)), maxFps: 24)
try await local.publishCamera(to: participant, options: options)
}
/// Start from a VP8 preset and change one setting.
@MainActor
func publishCameraWithLowerBitrate(to participant: RemoteParticipant) async throws {
guard let local = Room.shared.localParticipant else { return }
let preset = Vp8Encoding.lowLatency
let encoding = Vp8Encoding(
maxBitrate: 1_000_000,
cpuUsed: preset.cpuUsed,
quantizerMin: preset.quantizerMin,
quantizerMax: preset.quantizerMax,
keyframeInterval: preset.keyframeInterval,
rateControlMode: preset.rateControlMode,
cqLevel: preset.cqLevel,
temporalLayers: preset.temporalLayers
)
try await local.publishCamera(to: participant, options: VideoPublishOptions(codec: .vp8(encoding)))
}
```
## Video publish options
`VideoPublishOptions` has three fields:
| Field | Default | Meaning |
| --- | --- | --- |
| `codec` | `vp8AllKeyframe` | The codec and its settings. See [Codecs](#codecs). |
| `maxFps` | 15 | The most frames per second sent to this participant. Extra frames are dropped before encoding. |
| `degradationPreference` | `balanced` | Not supported yet; see [Degradation preference](#degradation-preference). |
`maxFps` limits sending, not capture. When the camera captures fewer frames than `maxFps`, the capture rate
is what you get; when it captures more, the extra frames are dropped.
## Codecs
`VideoCodec` is the codec together with its settings, so a VP8 codec always carries VP8 settings and an H.264
codec carries H.264 settings:
- `VideoCodec.vp8(_:)` takes a `Vp8Encoding`. The SDK encodes VP8 in software.
- `VideoCodec.h264(_:)` takes an `H264Encoding`. The device's hardware encoder encodes H.264.
`VideoCodec.type` gives the codec without its settings, as a `VideoCodecType` (`vp8` or `h264`). Use it to
compare codecs or as a dictionary key.
Five ready-made codecs:
| Codec | Codec type | Bitrate | Keyframe interval | Rate control |
| --- | --- | --- | --- | --- |
| `vp8AllKeyframe` (default) | VP8 | 3 Mbps | 1 | `vbr` |
| `vp8LowLatency` | VP8 | 3 Mbps | 15 | `cbr` |
| `vp8HighQuality` | VP8 | 8 Mbps | 60 | `vbr` |
| `vp8LowBandwidth` | VP8 | 1.5 Mbps | 10 | `cq` |
| `h264Default` | H.264 | 3 Mbps | — | — |
The VP8 ones are the `Vp8Encoding` presets of the same name: `Vp8Encoding.allKeyframe`, `Vp8Encoding.lowLatency`,
`Vp8Encoding.highQuality` and `Vp8Encoding.lowBandwidth`.
## Bitrate
`maxBitrate`, on both `Vp8Encoding` and `H264Encoding`, is the target bitrate in bits per second. The SDK passes
it straight to the encoder, which aims for it; it is not a ceiling the SDK enforces on top. When the connection
to the participant goes through a relay, the core lowers the bitrate to 1500 kbps on its own. The SDK does not
lower the resolution or the frame rate to fit a slow connection; see
[Degradation preference](#degradation-preference).
## VP8 settings
`Vp8Encoding` has a default for every field, and `Vp8Encoding()` is the same as `Vp8Encoding.allKeyframe`:
| Field | Range | `allKeyframe` | Meaning |
| --- | --- | --- | --- |
| `maxBitrate` | above 0 | 3,000,000 | Target bitrate in bits per second. See [Bitrate](#bitrate). |
| `cpuUsed` | -16 to 16 | 16 | Lower values compress better and use more CPU. |
| `quantizerMin` | 0 to 63 | 30 | The best quality the encoder may use. Lower is sharper and larger. |
| `quantizerMax` | 0 to 63, not below `quantizerMin` | 40 | The worst quality the encoder may use. |
| `keyframeInterval` | 1 and up | 1 | A keyframe every this many frames. 1 makes every frame a keyframe. |
| `rateControlMode` | `RcMode` | `vbr` | How the encoder trades bitrate for quality. |
| `cqLevel` | 0 to 63 | 30 | Target quality in `cq` mode; ignored in the other modes. |
| `temporalLayers` | 1 to 3 | 1 | See [Temporal layers](#temporal-layers). |
`RcMode` has three cases:
- `vbr`: aims for quality; the bitrate varies.
- `cbr`: keeps the bitrate steady; the quality varies.
- `cq`: aims for the quality in `cqLevel`, with the bitrate as a secondary limit.
A shorter `keyframeInterval` recovers from lost packets sooner but uses more bandwidth.
### Temporal layers
`temporalLayers` splits the frames into layers, so that a receiver could drop the upper layers and still decode
the rest at a lower frame rate:
- 1: no layers. Every frame is in the same layer.
- 2: the base layer is every second frame and gets half the bitrate; the second layer is the frames in between.
- 3: the base layer is every fourth frame and gets 40% of the bitrate; the second layer adds the frames halfway
between, up to 60%; the third layer is the remaining frames.
Changing the number of layers restarts the encoder, so the video stalls briefly. Nothing ever uses the layers:
the connection is peer to peer, so there is no server to drop layers for a slow participant, and the receiver
never drops layers either. Setting 2 or 3 only costs you the restart and a lower picture quality at the same
bitrate. Keep temporalLayers at 1.
## H.264 settings
`H264Encoding` has one field, `maxBitrate`, the target bitrate in bits per second (default 3 Mbps). See
[Bitrate](#bitrate).
## Change settings during a call
Each `LocalTrackPublication` keeps the options it was published with in `videoPublishOptions`, and has setters
that change them while the call goes on. They apply to that participant only, and the call is not interrupted:
- `LocalTrackPublication.setCodec(_:)` switches the codec, settings included.
- `LocalTrackPublication.setVp8Encoding(_:)` and `LocalTrackPublication.setH264Encoding(_:)` change the settings
without switching the codec. They throw `RoomError.codecNotSupported(codec:)` when the publication uses the
other codec.
- `LocalTrackPublication.setMaxFps(_:)` changes `maxFps`; it accepts 1 to 60.
- `LocalTrackPublication.requestKeyframe()` asks the encoder for a keyframe now.
Call them on the main thread. They throw `RoomError.trackNotPublished` once the publication is unpublished, and
`RoomError.invalidParameter(name:reason:)` for a bitrate of 0 or less or a frame rate out of range; see
[Errors](../core/errors.md#publishing).
=== "Swift"
```swift
import RevueSDK
/// Change what one participant receives while the call goes on. Each publication has its own
/// settings, so the other participants are not affected.
@MainActor
func sendSharperVideo(on publication: LocalTrackPublication) throws {
switch publication.videoPublishOptions?.codec.type {
case .vp8:
try publication.setVp8Encoding(.highQuality)
case .h264:
try publication.setH264Encoding(H264Encoding(maxBitrate: 6_000_000))
case nil:
return
}
try publication.setMaxFps(30)
}
```
## Audio publish options
`AudioPublishOptions` has one field, `encoding`, an `AudioEncoding` with a `maxBitrate` in bits per second.
`AudioEncoding` has three presets: `presetSpeech` (24 kbps), `presetMusic` (128 kbps) and
`presetMusicHighQuality` (192 kbps).
!!! note hint-unsupported "Not supported yet"
`AudioPublishOptions.encoding` has no effect yet: setting it does not change the audio bitrate.
## Degradation preference
`DegradationPreference` says what to give up first when bandwidth runs short: `balanced` (both),
`maintainResolution` (lower the frame rate first), `maintainFramerate` (lower the resolution first), or
`disabled`. You pass it as `VideoPublishOptions.degradationPreference`.
!!! note hint-unsupported "Not supported yet"
`VideoPublishOptions.degradationPreference` has no effect yet, whichever `DegradationPreference` you pass. The SDK does not lower the resolution or the frame rate when bandwidth runs short.
---
# Rendering video
`VideoView` shows a video track on screen: your own camera, a participant's video, a screen share or a track
you fill with your own frames. It is a `UIView`, so you add it to your layout like any other view and give it
a track.
`VideoView` is available on iOS only. There is no macOS version.
This page covers how to attach a track, how the picture fills the view, mirroring and rotation, the render
modes, pausing and freezing, the rendering state and its events, what the view shows while your camera is
muted, pinch to zoom, the debug overlay, custom renderers and SwiftUI.
## Show a track
Set `VideoView.track` to the track you want to show. The view starts drawing with the next frame; set it to
nil to stop and clear the view.
=== "Swift"
```swift
import RevueSDK
/// Show a participant's camera in their tile, or the App's placeholder when the camera is muted.
/// firstCameraVideoTrack still returns the track while the participant is muted, so check isMuted.
/// Call this again from didUpdateIsMuted.
func showCamera(of friendId: String, in videoView: VideoView) -> Bool {
let camera = Room.shared.remoteParticipants[friendId]?.firstCameraVideoTrack
guard let camera, !camera.isMuted else {
videoView.track = nil
return false // show the placeholder
}
videoView.track = camera
return true
}
```
### The view does not keep its track
`VideoView.track` is a weak reference: the view never keeps a track alive. Who keeps it depends on where the
track comes from:
| Track | Kept by |
| --- | --- |
| A remote participant's track | Its publication, while the participant publishes it. |
| `LocalParticipant.cameraTrack`, `microphoneTrack`, `screenShareTrack` | `LocalParticipant`, until you call `destroy()`. |
| A track from `LocalVideoTrack.createBufferTrack(name:source:)` | Your App. Keep it in a property for as long as you show it. |
When the last owner lets a track go, for example when a participant leaves, the track is released,
`VideoView.track` becomes nil and the view clears. A track you created yourself and did not store is released
straight away, and the view stays blank.
Set `VideoView.track` to nil when the view is no longer shown, for example when a collection view cell is
reused, so that the view stops drawing a track nobody sees.
=== "Swift"
```swift
import RevueSDK
import UIKit
/// Preview a track the App creates itself. VideoView holds its track weakly: keep the track in a
/// property, or it is released and the view goes blank. Clear the view when it is no longer shown.
@MainActor
final class OwnTrackPreview {
let track = LocalVideoTrack.createBufferTrack(name: "ipcam", source: .camera)
let videoView = VideoView(frame: .zero)
func show() {
videoView.track = track
}
func hide() {
videoView.track = nil
}
}
```
## Fill the view
`VideoView.layoutMode` decides how a picture whose shape differs from the view's is placed:
| `VideoView.LayoutMode` | Result |
| --- | --- |
| `.fill` (default) | Fills the whole view and crops what sticks out. No black bars. |
| `.fit` | Shows the whole picture, with black bars where it does not cover the view. |
The view's background is black, so bars and an empty view are black.
## Mirror and rotate
`VideoView.mirrorMode` mirrors the picture on this screen only. The participants you send to do not see the
mirroring; to mirror what you send, use `CameraCaptureOptions.isMirrored` (see
[Camera and capture options](capture-options.md#camera-capture-options)).
| `VideoView.MirrorMode` | Result |
| --- | --- |
| `.auto` (default) | Mirrors your own front camera, as people expect from a selfie preview. Your back camera, remote tracks, screen shares and buffer tracks are not mirrored. When you switch cameras, the view follows. |
| `.off` | Never mirrors. |
| `.mirror` | Always mirrors. |
`VideoView.rotationOverride` turns the picture clockwise by a `VideoRotation` (`._90`, `._180` or `._270`).
Frames reach the view already upright: the sender turns the pixels upright before sending (see
[Your own frames and audio](capture-options.md#push-frames)). `rotationOverride` is an extra angle your App
chooses for display, for example to match a screen mounted on its side. nil, the default, and `._0` show the
picture as it is.
- `layoutMode` works on the rotated picture: a 1280×720 picture turned by `._90` fills or fits the view as a
720×1280 one.
- Rotation and mirroring add up: the picture is turned first, then mirrored.
- With the Metal renderer the new angle applies from the next frame; with the sample buffer renderer it applies
as soon as you set it. See [Render modes](#render-modes).
- You can set it while frames arrive.
## Render modes
`VideoView.renderMode` picks how the view draws:
| `VideoView.RenderMode` | Draws with |
| --- | --- |
| `.auto` (default) | Metal when the device supports it, otherwise an `AVSampleBufferDisplayLayer`. |
| `.metal` | Metal. Falls back to the sample buffer layer when Metal is not available. |
| `.sampleBuffer` | An `AVSampleBufferDisplayLayer`. |
`VideoView.isMetalAvailable()` tells whether Metal can be used. Changing the mode clears the view; the next
frame draws with the new mode.
`VideoView.avSampleBufferDisplayLayer` gives the sample buffer layer, for example to show the video in
picture in picture, which accepts this kind of layer only. Set `renderMode` to `.sampleBuffer` first: in Metal
mode the property is nil.
## Pause, hide and freeze
Three properties stop the view from drawing new frames. None of them affects the track: frames keep arriving,
and the participants you send to see no difference.
| Property | While set | The view shows |
| --- | --- | --- |
| `isEnabled = false` | New frames are dropped. | Nothing: the view is cleared. |
| `isHidden = true` | New frames are dropped. | Nothing: the view is hidden and cleared. |
| `isFrozen = true` | New frames are dropped. | The last frame, kept on screen. |
`isEnabled` suits a view that still exists but cannot be seen, such as a reused cell. When `isEnabled` goes back
to true or the view is shown again, it draws from the next frame on.
A frozen view counts as not rendering after 2 seconds (see below). `isFrozen` belongs to the view, not to the
track: when the track changes or the view is cleared, the frozen picture goes away but `isFrozen` stays true,
so the view stays blank until you set it back to false.
## Rendering state and events
Two properties tell whether video is showing:
- `VideoView.isRendering` is true while frames are being drawn. It turns true with the first frame and false
when no new frame has been drawn for 2 seconds, when the view is cleared, or when the track changes.
- `VideoView.didRenderFirstFrame` turns true with the first frame drawn and stays true until the track changes.
A track can exist before its first frame arrives, for example while a remote participant's video is still
connecting. Use `isRendering` to decide when to show a placeholder over the view.
A `VideoViewDelegate` hears about two changes:
| Event | When |
| --- | --- |
| `videoView(_:didUpdate:)` with a `Bool` | `isRendering` changed. |
| `videoView(_:didUpdate:)` with a `CGSize` | The view's size changed, in points. Not sent when the view only moves, nor for its first layout when the size equals the frame you created it with. |
Add a delegate with `VideoView.add(delegate:)` and remove it with `VideoView.remove(delegate:)` or
`VideoView.removeAllDelegates()`; `VideoView.delegates` is the same list. Delegates are held weakly. Like every
delegate in RevueSDK, both events arrive on the main thread, even when the frame was drawn on another thread.
=== "Swift"
```swift
import RevueSDK
import UIKit
/// Cover a tile with a placeholder until its video shows, and follow the tile's size.
/// Both events arrive on the main thread.
@MainActor
final class TileController: @MainActor VideoViewDelegate {
let videoView = VideoView(frame: .zero)
let placeholder = UIView()
init() {
videoView.add(delegate: self) // held weakly
}
func videoView(_ videoView: VideoView, didUpdate isRendering: Bool) {
placeholder.isHidden = isRendering
}
func videoView(_ videoView: VideoView, didUpdate size: CGSize) {
// The new size of the view in points; not sent when it only moves.
print("tile is now \(size.width)×\(size.height)")
}
func stopWatching() {
videoView.remove(delegate: self)
}
}
```
## When your camera is muted
For your own camera, the view shows `LocalVideoTrack.mutePlaceholder` while the track is muted or the camera
is off, the same picture the participants you send to receive.
- With a `mutePlaceholder` set, the view shows it, and changing it while muted shows the new one at once.
- With no `mutePlaceholder`, the view is cleared: it does not keep the last camera frame. Setting
`mutePlaceholder` to nil while muted clears the view.
- When you unmute or turn the camera on, camera frames replace the placeholder.
A view you attach while the camera is already muted or off starts with the placeholder, or empty.
A [custom renderer](#custom-renderers) is not told to clear. It receives the placeholder through
`render(pixelBuffer:)` like any frame, but when there is no placeholder it gets nothing and keeps whatever it
drew last; watch the track's mute state yourself to clear it.
For a remote track, the view clears when the track becomes muted or stops.
## Pinch to zoom
On your own camera, the user can pinch the view to zoom. Pinching changes the camera's own zoom
(`CameraCapturer.device`), not the picture on screen, so the participants you send to see the zoomed picture
too. On a remote participant's video, a screen share or a buffer track, pinching does nothing.
Pinch to zoom is off by default. Turn it on with `VideoView.isPinchToZoomEnabled` or
`VideoView.pinchToZoomOptions`, a `VideoView.PinchToZoomOptions`:
| Option | Effect |
| --- | --- |
| `.zoomIn` | The user can zoom in beyond the camera's default zoom, up to the camera's maximum. |
| `.zoomOut` | The user can zoom out below the default zoom, down to the camera's minimum. |
| `.resetOnRelease` | When the user lifts their fingers, the zoom goes back to the default. |
Without `.zoomIn` and `.zoomOut` pinching does nothing. Without `.zoomOut` the zoom never goes below the
default, and without `.zoomIn` never above it.
- `isPinchToZoomEnabled = true` adds `.zoomIn` only: the user can zoom in, not out. Setting it to false removes
`.zoomIn` only; reading it returns true while either `.zoomIn` or `.zoomOut` is set.
- `VideoView.isAutoZoomResetEnabled` adds or removes `.resetOnRelease`.
- The default zoom is 2 on cameras made of several lenses whose 1× is the ultra wide lens (triple and dual wide
cameras), so that 2× matches the wide lens. It is 1 on other cameras, kept within what the camera supports.
- When you change the options, the camera's current zoom is brought back within the new range, or to the
default when `.resetOnRelease` is set.
## Debug overlay
Set `VideoView.isDebugMode` to true to draw an overlay on the view, for tracking down video problems. It shows:
| Line | Meaning |
| --- | --- |
| sid | The track's sid, or nil without a track. |
| resolution | Width × height of the last frame drawn, or `-` before the first frame. |
| firstFrame | `didRenderFirstFrame`. |
| isRendering | `isRendering`. |
| renderMode | The `renderMode` you set, and the renderer actually used (`metal` or `sampleBuffer`). |
| renderers | How many renderers the track has, this view included. |
| FPS | Frames drawn in the last second, updated every second. |
The overlay's border is green while the view is rendering and red when it is not. The overlay does not take
touches. Setting `isDebugMode` back to false removes it.
## Adaptive stream
Four members belong together: `VideoView.isAdaptiveStreamEnabled`, `VideoView.adaptiveStreamSize`,
`VideoView.adaptiveStreamPixelDensity` and `VideoView.set(size:)`. Together they let the participant whose video
you show send it at the size you display it, instead of always at their capture size.
!!! note hint-unsupported "Not supported yet"
Adaptive stream has no effect yet: the participant you show keeps sending at their capture size, whatever the size of your `VideoView`. `isAdaptiveStreamEnabled` cannot be set yet, and `set(size:)` does nothing.
- The view tells the participant it shows how big it is. The size is `adaptiveStreamSize`: the view's size times
the pixel density, in pixels.
- `adaptiveStreamPixelDensity` is the density: `.auto` (default) uses the screen's scale, `.fixed(_:)` a value
of your own. Either is capped at `VideoView.AdaptiveStreamPixelDensity.maxDensity` (3), so a large screen does
not ask for more than it needs.
- A view whose `isAdaptiveStreamEnabled` is false is not counted. A view that has not been laid out yet, is
hidden or has `isEnabled` false always reads false.
- The view follows its own size from layout, so you do not need to call `set(size:)`.
## Custom renderers
To get decoded frames yourself, for example to draw them with your own engine, implement `VideoRenderer` and add
it to a track with `VideoTrack.add(videoRenderer:)`; remove it with `VideoTrack.remove(videoRenderer:)`. The track
holds renderers weakly, so keep yours. `VideoView` is a `VideoRenderer` itself, and the track adds it when you set
`VideoView.track`.
`VideoRenderer` has one method, `render(pixelBuffer:)`. The track calls it once per frame; your App never calls
it on a `VideoView`. It is called on the thread that delivers the frame, not on the main thread: the camera's
capture queue for your camera, the thread your App pushes buffers from for a buffer track, and the receiving
thread for a remote track. Return quickly and move work to the main thread yourself when you touch UI.
A custom renderer receives frames only. It is not told when the view would be cleared; see
[When your camera is muted](#when-your-camera-is-muted).
## SwiftUI
RevueSDK has no SwiftUI view. Wrap `VideoView` in a `UIViewRepresentable`:
=== "Swift"
```swift
import RevueSDK
import SwiftUI
/// Show a video track in SwiftUI. RevueSDK has no SwiftUI view of its own: wrap VideoView in a
/// UIViewRepresentable. The view does not keep the track alive, so whoever passes it in must hold it.
struct TrackVideoView: UIViewRepresentable {
let track: VideoTrack?
var layoutMode: VideoView.LayoutMode = .fill
var mirrorMode: VideoView.MirrorMode = .auto
func makeUIView(context: Context) -> VideoView {
VideoView(frame: .zero)
}
func updateUIView(_ view: VideoView, context: Context) {
view.layoutMode = layoutMode
view.mirrorMode = mirrorMode
// Setting the same track again restarts it (first frame, rendering state), so only set a new one.
if view.track !== track {
view.track = track
}
}
static func dismantleUIView(_ view: VideoView, coordinator: ()) {
view.track = nil
}
}
```
As with `VideoView` itself, the wrapper does not keep the track: pass in a track that something else holds.
---
# Audio
> **Writing in progress:** This topic is still being written. It will be filled in a later update.
---
# Screen sharing
> **Writing in progress:** This topic is still being written. It will be filled in a later update.
---
# Encryption
> **Writing in progress:** This topic is still being written. It will be filled in a later update.
---
# Friends
> **Writing in progress:** This topic is still being written. It will be filled in a later update.
---
# Presence
> **Writing in progress:** This topic is still being written. It will be filled in a later update.
---
# Recording
> **Writing in progress:** This topic is still being written. It will be filled in a later update.
---
# Compositing
> **Writing in progress:** This topic is still being written. It will be filled in a later update.
---
# capture(_:)
[RevueSDK](../../index.md) › [AudioBufferCapturer](../index.md) › capture(_:)
*Instance Method*
注入一幀音訊(AVAudioPCMBuffer)
```swift
func capture(_ pcmBuffer: AVAudioPCMBuffer)
```
## Discussion
同時 dispatch 給 track 的所有 AudioRenderer(本地旁聽)與 publish 訂閱者(發送)。
---
# captureState
[RevueSDK](../../index.md) › [AudioBufferCapturer](../index.md) › captureState
*Instance Property*
```swift
var captureState: CaptureState { get }
```
---
# AudioBufferCapturer
[RevueSDK](../index.md) › AudioBufferCapturer
*Class*
Audio Buffer 擷取器 — 讓 App 注入任意 PCM 音訊
```swift
class AudioBufferCapturer
```
## Overview
```swift
let audioTrack = LocalAudioTrack.createBufferTrack()
let capturer = audioTrack.capturer as! AudioBufferCapturer
// IPCam 音訊轉發
ipcam.onAudioFrame = { pcmBuffer in
capturer.capture(pcmBuffer)
}
// 音樂檔注入
let pcmBuffer = loadAudioFile("bgm.wav")
capturer.capture(pcmBuffer)
```
## Topics
### Instance Properties
- [`var captureState: CaptureState`](capturestate/index.md)
### Instance Methods
- [`func capture(AVAudioPCMBuffer)`](capture%28_:%29/index.md)
注入一幀音訊(AVAudioPCMBuffer)
- [`func startCapture() async throws`](startcapture%28%29/index.md)
- [`func stopCapture() async throws`](stopcapture%28%29/index.md)
## Relationships
### Conforms To
- [`AudioCapturer`](../audiocapturer/index.md)
---
# startCapture()
[RevueSDK](../../index.md) › [AudioBufferCapturer](../index.md) › startCapture()
*Instance Method*
```swift
func startCapture() async throws
```
---
# stopCapture()
[RevueSDK](../../index.md) › [AudioBufferCapturer](../index.md) › stopCapture()
*Instance Method*
```swift
func stopCapture() async throws
```
---
# captureState
[RevueSDK](../../index.md) › [AudioCapturer](../index.md) › captureState
*Instance Property*
擷取器狀態
```swift
var captureState: CaptureState { get }
```
---
# AudioCapturer
[RevueSDK](../index.md) › AudioCapturer
*Protocol*
音訊擷取器基礎協定
```swift
protocol AudioCapturer : AnyObject
```
## Overview
只有 SDK 建的 `MicrophoneCapturer`、`AudioBufferCapturer` 會實作;App 不自己實作 (自訂音訊來源一律用 `LocalAudioTrack.createBufferTrack`,spec #86)。
## Topics
### Instance Properties
- [`var captureState: CaptureState`](capturestate/index.md)
擷取器狀態
### Instance Methods
- [`func startCapture() async throws`](startcapture%28%29/index.md)
啟動擷取
- [`func stopCapture() async throws`](stopcapture%28%29/index.md)
停止擷取
## Relationships
### Conforming Types
- [`MicrophoneCapturer`](../microphonecapturer/index.md)
- [`AudioBufferCapturer`](../audiobuffercapturer/index.md)
---
# startCapture()
[RevueSDK](../../index.md) › [AudioCapturer](../index.md) › startCapture()
*Instance Method*
啟動擷取
```swift
func startCapture() async throws
```
---
# stopCapture()
[RevueSDK](../../index.md) › [AudioCapturer](../index.md) › stopCapture()
*Instance Method*
停止擷取
```swift
func stopCapture() async throws
```
---
# addTrack(_:)
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › addTrack(_:)
*Instance Method*
加入音訊 Track 到混音
```swift
func addTrack(_ track: AudioTrack)
```
## Parameters
- track
要加入混音的 AudioTrack
## Discussion
內部為此 Track 建立對應的 TrackAudioReceiver(AudioRenderer), 監聽 Track 的 PCM callback,收到後餵入 AudioMixer。
---
# autoOutputSilentBuffer
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › autoOutputSilentBuffer
*Instance Property*
啟動無聲音頻輸出(在沒有任何輸入時也補幀輸出),預設 false
```swift
var autoOutputSilentBuffer: Bool { get set }
```
## Discussion
錄影場景建議開啟,確保音軌連續不中斷。
---
# AudioCompositor
[RevueSDK](../index.md) › AudioCompositor
*Class*
音訊混音器 — 將多路 AudioTrack 混音為一路輸出(獨立工具)
```swift
class AudioCompositor
```
## Overview
不綁定 Room,用戶自己建立、管理輸入、發布。 內部使用 AVAudioEngine 離線手動渲染模式(offline manual rendering)進行混音。
MCU 場景需要為每個參與者建立獨立的 AudioCompositor(個別化混音,排除該人自己的聲音)。
```swift
// === 四人 MCU(Host + A, B, C)完整案例 ===
var mixers: [String: AudioCompositor] = [:] // 給每個人的專屬混音
var localMix = AudioCompositor() // 本地播放用
try localMix.start()
// --- 參與者加入 ---
func room(_ room: Room, participantDidConnect participant: RemoteParticipant) {
// 1. 為新人建立專屬混音器(排除新人自己的聲音)
let mixer = AudioCompositor()
try mixer.start()
mixer.addTrack(localParticipant.microphoneTrack) // Host 自己的聲音
// 加入其他已在房間的人的聲音
for (id, remote) in room.remoteParticipants where id != participant.identity {
mixer.addTrack(remote.audioTrack)
}
mixers[participant.identity] = mixer
// 2. 把新人的聲音加進其他人的混音器
for (id, existingMixer) in mixers where id != participant.identity {
existingMixer.addTrack(participant.audioTrack)
}
// 3. 本地播放也加入新人
localMix.addTrack(participant.audioTrack)
// 4. 發布給該參與者
localParticipant.publish(audioTrack: mixer.outputTrack, to: participant)
}
// --- 參與者離開 ---
func room(_ room: Room, participantDidDisconnect participant: RemoteParticipant) {
// 1. 停止並移除此人的專屬混音器
if let mixer = mixers.removeValue(forKey: participant.identity) {
mixer.stop()
}
// 2. 從其他人的混音器中移除此人的聲音
for (_, existingMixer) in mixers {
existingMixer.removeTrack(participant.audioTrack)
}
// 3. 本地播放也移除
localMix.removeTrack(participant.audioTrack)
}
// --- 離開房間:完整釋放所有資源 ---
func leaveRoom() {
// 1. 停止所有專屬混音器
for (_, mixer) in mixers {
mixer.stop()
}
mixers.removeAll()
// 2. 停止本地播放混音器
localMix.stop()
// 3. 斷開所有遠端參與者
Room.shared.disconnectAll()
}
// --- 穩定後四人房間狀態 ---
// mixers["A"] = Host + B + C → publish to A
// mixers["B"] = Host + A + C → publish to B
// mixers["C"] = Host + A + B → publish to C
// localMix = A + B + C → 本地播放
```
## Topics
### Initializers
- [`init()`](init%28%29/index.md)
### Instance Properties
- [`var autoOutputSilentBuffer: Bool`](autooutputsilentbuffer/index.md)
啟動無聲音頻輸出(在沒有任何輸入時也補幀輸出),預設 false
- [`var isRunning: Bool`](isrunning/index.md)
混音器是否運行中(唯讀)
- [`var localPlaybackEnabled: Bool`](localplaybackenabled/index.md)
是否將混音結果送到本地喇叭播放(測試/監聽用,預設 false)
- [`var onMixerOutput: ((String) -> Void)?`](onmixeroutput/index.md)
mixer output 診斷 callback(可選,供 UI 顯示混音器輸出狀態)
- [`var outputTrack: LocalAudioTrack?`](outputtrack/index.md)
混音輸出 Track(用於本地播放或發布)
- [`var tracks: [AudioTrack]`](tracks/index.md)
當前所有輸入 Track(唯讀)
### Instance Methods
- [`func addTrack(AudioTrack)`](addtrack%28_:%29/index.md)
加入音訊 Track 到混音
- [`func removeTrack(AudioTrack)`](removetrack%28_:%29/index.md)
從混音中移除音訊 Track
- [`func start() throws`](start%28%29/index.md)
啟動混音器
- [`func stop()`](stop%28%29/index.md)
停止混音器,釋放所有資源
---
# init()
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › init()
*Initializer*
```swift
init()
```
---
# isRunning
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › isRunning
*Instance Property*
混音器是否運行中(唯讀)
```swift
@Published var isRunning: Bool { get set }
```
---
# localPlaybackEnabled
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › localPlaybackEnabled
*Instance Property*
是否將混音結果送到本地喇叭播放(測試/監聽用,預設 false)
```swift
var localPlaybackEnabled: Bool
```
---
# objectWillChange
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › objectWillChange
*Instance Property*
- iOS 13.0+
- iPadOS 13.0+
- macCatalyst 13.0+
- macOS 10.15+
- tvOS 13.0+
- watchOS 6.0+
```swift
var objectWillChange: ObservableObjectPublisher { get }
```
---
# ObservableObject Implementations
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › ObservableObject Implementations
*Article*
---
# onMixerOutput
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › onMixerOutput
*Instance Property*
mixer output 診斷 callback(可選,供 UI 顯示混音器輸出狀態)
```swift
var onMixerOutput: ((String) -> Void)?
```
---
# outputTrack
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › outputTrack
*Instance Property*
混音輸出 Track(用於本地播放或發布)
```swift
var outputTrack: LocalAudioTrack? { get }
```
## Discussion
```swift
// 本地播放
localMix.outputTrack // → 自動播放
// 發布到指定對象
localParticipant.publish(audioTrack: mixer.outputTrack, to: participant)
```
---
# removeTrack(_:)
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › removeTrack(_:)
*Instance Method*
從混音中移除音訊 Track
```swift
func removeTrack(_ track: AudioTrack)
```
## Parameters
- track
要移除的 AudioTrack
## Discussion
停止監聽並清理對應的 PlayerNode 資源。
---
# start()
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › start()
*Instance Method*
啟動混音器
```swift
func start() throws
```
## Discussion
內部使用 AVAudioEngine 離線手動渲染模式。
> **Throws**
>
> AVAudioEngine 啟動失敗時拋出錯誤
---
# stop()
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › stop()
*Instance Method*
停止混音器,釋放所有資源
```swift
func stop()
```
---
# tracks
[RevueSDK](../../index.md) › [AudioCompositor](../index.md) › tracks
*Instance Property*
當前所有輸入 Track(唯讀)
```swift
var tracks: [AudioTrack] { get }
```
---
# !=(_:_:)
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# AudioDevice.DeviceType.bluetooth
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › AudioDevice.DeviceType.bluetooth
*Case*
```swift
case bluetooth
```
---
# AudioDevice.DeviceType.builtInMicrophone
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › AudioDevice.DeviceType.builtInMicrophone
*Case*
```swift
case builtInMicrophone
```
---
# AudioDevice.DeviceType.builtInReceiver
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › AudioDevice.DeviceType.builtInReceiver
*Case*
```swift
case builtInReceiver
```
---
# AudioDevice.DeviceType.builtInSpeaker
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › AudioDevice.DeviceType.builtInSpeaker
*Case*
```swift
case builtInSpeaker
```
---
# Equatable Implementations
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# AudioDevice.DeviceType
[RevueSDK](../../index.md) › [AudioDevice](../index.md) › AudioDevice.DeviceType
*Enumeration*
```swift
enum DeviceType
```
## Topics
### Enumeration Cases
- [`case bluetooth`](bluetooth/index.md)
- [`case builtInMicrophone`](builtinmicrophone/index.md)
- [`case builtInReceiver`](builtinreceiver/index.md)
- [`case builtInSpeaker`](builtinspeaker/index.md)
- [`case unknown`](unknown/index.md)
- [`case wiredHeadset`](wiredheadset/index.md)
### Initializers
- [`init?(rawValue: String)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: String)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › RawRepresentable Implementations
*Article*
---
# AudioDevice.DeviceType.unknown
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › AudioDevice.DeviceType.unknown
*Case*
```swift
case unknown
```
---
# AudioDevice.DeviceType.wiredHeadset
[RevueSDK](../../../index.md) › [AudioDevice](../../index.md) › [AudioDevice.DeviceType](../index.md) › AudioDevice.DeviceType.wiredHeadset
*Case*
```swift
case wiredHeadset
```
---
# id
[RevueSDK](../../index.md) › [AudioDevice](../index.md) › id
*Instance Property*
```swift
let id: String
```
---
# AudioDevice
[RevueSDK](../index.md) › AudioDevice
*Structure*
音訊裝置資訊
```swift
struct AudioDevice
```
## Topics
### Initializers
- [`init(id: String, name: String, type: DeviceType)`](init%28id:name:type:%29/index.md)
### Instance Properties
- [`let id: String`](id/index.md)
- [`let name: String`](name/index.md)
- [`let type: DeviceType`](type/index.md)
### Enumerations
- [`enum DeviceType`](devicetype/index.md)
---
# init(id:name:type:)
[RevueSDK](../../index.md) › [AudioDevice](../index.md) › init(id:name:type:)
*Initializer*
```swift
init(id: String, name: String, type: DeviceType)
```
---
# name
[RevueSDK](../../index.md) › [AudioDevice](../index.md) › name
*Instance Property*
```swift
let name: String
```
---
# type
[RevueSDK](../../index.md) › [AudioDevice](../index.md) › type
*Instance Property*
```swift
let type: DeviceType
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [AudioDuckingLevel](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# AudioDuckingLevel.default
[RevueSDK](../../index.md) › [AudioDuckingLevel](../index.md) › AudioDuckingLevel.default
*Case*
```swift
case `default`
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [AudioDuckingLevel](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../index.md) › [AudioDuckingLevel](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [AudioDuckingLevel](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# AudioDuckingLevel
[RevueSDK](../index.md) › AudioDuckingLevel
*Enumeration*
```swift
enum AudioDuckingLevel
```
## Overview
> **Note**
>
> 目前 `AudioManager.duckingLevel` 標為 unavailable、尚未實作。
## Topics
### Enumeration Cases
- [`case `default``](default/index.md)
- [`case max`](max/index.md)
- [`case mid`](mid/index.md)
- [`case min`](min/index.md)
### Initializers
- [`init?(rawValue: Int)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../index.md) › [AudioDuckingLevel](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [AudioDuckingLevel](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# AudioDuckingLevel.max
[RevueSDK](../../index.md) › [AudioDuckingLevel](../index.md) › AudioDuckingLevel.max
*Case*
```swift
case max
```
---
# AudioDuckingLevel.mid
[RevueSDK](../../index.md) › [AudioDuckingLevel](../index.md) › AudioDuckingLevel.mid
*Case*
```swift
case mid
```
---
# AudioDuckingLevel.min
[RevueSDK](../../index.md) › [AudioDuckingLevel](../index.md) › AudioDuckingLevel.min
*Case*
```swift
case min
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [AudioDuckingLevel](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [AudioDuckingLevel](../index.md) › RawRepresentable Implementations
*Article*
---
# AudioEncoding
[RevueSDK](../index.md) › AudioEncoding
*Structure*
音訊編碼設定
```swift
struct AudioEncoding
```
## Overview
```swift
AudioEncoding(maxBitrate: 64_000) // 64 Kbps
```
## Topics
### Initializers
- [`init(maxBitrate: Int)`](init%28maxbitrate:%29/index.md)
### Instance Properties
- [`let maxBitrate: Int`](maxbitrate/index.md)
最大位元率 (bps)
### Type Properties
- [`static let presetMusic: AudioEncoding`](presetmusic/index.md)
- [`static let presetMusicHighQuality: AudioEncoding`](presetmusichighquality/index.md)
- [`static let presetSpeech: AudioEncoding`](presetspeech/index.md)
---
# init(maxBitrate:)
[RevueSDK](../../index.md) › [AudioEncoding](../index.md) › init(maxBitrate:)
*Initializer*
```swift
init(maxBitrate: Int)
```
---
# maxBitrate
[RevueSDK](../../index.md) › [AudioEncoding](../index.md) › maxBitrate
*Instance Property*
最大位元率 (bps)
```swift
let maxBitrate: Int
```
---
# presetMusic
[RevueSDK](../../index.md) › [AudioEncoding](../index.md) › presetMusic
*Type Property*
```swift
static let presetMusic: AudioEncoding
```
---
# presetMusicHighQuality
[RevueSDK](../../index.md) › [AudioEncoding](../index.md) › presetMusicHighQuality
*Type Property*
```swift
static let presetMusicHighQuality: AudioEncoding
```
---
# presetSpeech
[RevueSDK](../../index.md) › [AudioEncoding](../index.md) › presetSpeech
*Type Property*
```swift
static let presetSpeech: AudioEncoding
```
---
# AudioManager
[RevueSDK](../index.md) › AudioManager
*Class*
音訊管理器 — 裝置管理、說話偵測、音量監控
```swift
class AudioManager
```
## Overview
```swift
let audioManager = AudioManager.shared
audioManager.onDeviceUpdate = { devices in
print("音訊設備變更")
}
audioManager.onMutedSpeechActivity = {
print("使用者被靜音但正在說話")
}
```
## Topics
### Instance Properties
- [`var inputDevice: AudioDevice?`](inputdevice/index.md)
當前輸入裝置(麥克風)
- [`var inputDevices: [AudioDevice]`](inputdevices/index.md)
所有可用輸入裝置
- [`var isMicrophoneMuted: Bool`](ismicrophonemuted/index.md)
- [`var isOtherAudioPlaying: Bool`](isotheraudioplaying/index.md)
是否有其他 App 正在播放音訊
- [`var isSpeakerOutputPreferred: Bool`](isspeakeroutputpreferred/index.md)
是否偏好揚聲器輸出(`false` = 內建聽筒)
- [`var localAudioFrameCount: Int`](localaudioframecount/index.md)
本機已發送的 PCM frame 數量
- [`var onDeviceUpdate: (([AudioDevice]) -> Void)?`](ondeviceupdate/index.md)
音訊設備變更回調(插入/拔出耳機等)
- [`var onInterruptionBegan: (() -> Void)?`](oninterruptionbegan/index.md)
音訊中斷開始回調(電話來電、鬧鐘等)
- [`var onInterruptionEnded: ((_ shouldResume: Bool) -> Void)?`](oninterruptionended/index.md)
音訊中斷結束回調
- [`var onMutedSpeechActivity: (() -> Void)?`](onmutedspeechactivity/index.md)
偵測使用者被靜音時在說話
- [`var onOtherAudioPlayingChange: ((Bool) -> Void)?`](onotheraudioplayingchange/index.md)
其他 App 的主音訊開始/結束播放時回調(帶當下的 [isOtherAudioPlaying])。
- [`var outputDevice: AudioDevice?`](outputdevice/index.md)
當前輸出裝置(揚聲器/耳機)
- [`var outputDevices: [AudioDevice]`](outputdevices/index.md)
所有可用輸出裝置
- [`var remoteAudioFrameCount: Int`](remoteaudioframecount/index.md)
收到遠端的 PCM frame 數量
### Type Properties
- [`static let shared: AudioManager`](shared/index.md)
Singleton
- [`static let standardChannelCount: Int`](standardchannelcount/index.md)
SDK 標準聲道數(1 = mono)
- [`static let standardFormat: AVAudioFormat`](standardformat/index.md)
SDK 標準音訊格式(Float32, 24000Hz, 1ch mono)
- [`static let standardSampleRate: Double`](standardsamplerate/index.md)
SDK 標準音訊取樣率(24000 Hz)
---
# inputDevice
[RevueSDK](../../index.md) › [AudioManager](../index.md) › inputDevice
*Instance Property*
當前輸入裝置(麥克風)
```swift
var inputDevice: AudioDevice? { get set }
```
## Discussion
設值即選擇偏好輸入裝置;設 `nil`(或指定的裝置已不存在)回系統預設。
**切換前後必須停/啟硬體**(對齊 ios-arctos-sdk/AudioSessionTool 的 `setPreferredInput` → `startRecording` / `startManualPlay` 流程)。
這不是保險,是必要的:`AVAudioEngine` 的節點連接在 `start()` 當下就 **綁定了那一刻的硬體格式**(取樣率、聲道數),換裝置後 engine 仍掛在 舊格式上,不會自己重新協商。唯有重啟才會重新去問系統「現在的輸入是誰、 格式是什麼」(見 `SpeakerPlayer.start()` 的 `connect(..., format: nil)` —— nil 即「用當下硬體格式」,只在 connect 那一刻求值)。
---
# inputDevices
[RevueSDK](../../index.md) › [AudioManager](../index.md) › inputDevices
*Instance Property*
所有可用輸入裝置
```swift
var inputDevices: [AudioDevice] { get }
```
---
# isMicrophoneMuted
[RevueSDK](../../index.md) › [AudioManager](../index.md) › isMicrophoneMuted
*Instance Property*
```swift
var isMicrophoneMuted: Bool { get set }
```
---
# isOtherAudioPlaying
[RevueSDK](../../index.md) › [AudioManager](../index.md) › isOtherAudioPlaying
*Instance Property*
是否有其他 App 正在播放音訊
```swift
var isOtherAudioPlaying: Bool { get }
```
---
# isSpeakerOutputPreferred
[RevueSDK](../../index.md) › [AudioManager](../index.md) › isSpeakerOutputPreferred
*Instance Property*
是否偏好揚聲器輸出(`false` = 內建聽筒)
```swift
var isSpeakerOutputPreferred: Bool { get set }
```
## Discussion
設值後會下沉到 session queue 套用;route 變更、中斷結束、重設 category、 speaker 重啟後也會自動重套(override 會被系統重置)。
---
# localAudioFrameCount
[RevueSDK](../../index.md) › [AudioManager](../index.md) › localAudioFrameCount
*Instance Property*
本機已發送的 PCM frame 數量
```swift
@Published var localAudioFrameCount: Int { get set }
```
---
# onDeviceUpdate
[RevueSDK](../../index.md) › [AudioManager](../index.md) › onDeviceUpdate
*Instance Property*
音訊設備變更回調(插入/拔出耳機等)
```swift
var onDeviceUpdate: (([AudioDevice]) -> Void)?
```
---
# onInterruptionBegan
[RevueSDK](../../index.md) › [AudioManager](../index.md) › onInterruptionBegan
*Instance Property*
音訊中斷開始回調(電話來電、鬧鐘等)
```swift
var onInterruptionBegan: (() -> Void)?
```
---
# onInterruptionEnded
[RevueSDK](../../index.md) › [AudioManager](../index.md) › onInterruptionEnded
*Instance Property*
音訊中斷結束回調
```swift
var onInterruptionEnded: ((_ shouldResume: Bool) -> Void)?
```
## Parameters
- shouldResume
系統是否建議恢復播放。`false` 代表中斷源仍 持有音訊焦點(例如通話尚未結束),此時強行播放通常無聲。
## Discussion
SDK 端的 session 資源**一律**會自行重配(與此值無關);這個參數是給 應用層決定「要不要恢復自己的播放」用的。
---
# onMutedSpeechActivity
[RevueSDK](../../index.md) › [AudioManager](../index.md) › onMutedSpeechActivity
*Instance Property*
偵測使用者被靜音時在說話
```swift
var onMutedSpeechActivity: (() -> Void)?
```
## Discussion
> **Note**
>
> 目前尚未接上偵測來源(需 iOS 17+ voice processing),設值不會被觸發。 待 VP 相容性驗證後實作,見 `duckingLevel` 說明。
---
# onOtherAudioPlayingChange
[RevueSDK](../../index.md) › [AudioManager](../index.md) › onOtherAudioPlayingChange
*Instance Property*
其他 App 的主音訊開始/結束播放時回調(帶當下的 [isOtherAudioPlaying])。
```swift
var onOtherAudioPlayingChange: ((Bool) -> Void)?
```
## Discussion
供應用層決定混音策略 —— 例如對方在放音樂時降低自己的音量、暫停背景音。 SDK 只轉發、不自行改變行為。
沒有這個通知的話 `isOtherAudioPlaying` 只能主動輪詢、無從得知何時該問。
---
# outputDevice
[RevueSDK](../../index.md) › [AudioManager](../index.md) › outputDevice
*Instance Property*
當前輸出裝置(揚聲器/耳機)
```swift
var outputDevice: AudioDevice? { get }
```
---
# outputDevices
[RevueSDK](../../index.md) › [AudioManager](../index.md) › outputDevices
*Instance Property*
所有可用輸出裝置
```swift
var outputDevices: [AudioDevice] { get }
```
---
# remoteAudioFrameCount
[RevueSDK](../../index.md) › [AudioManager](../index.md) › remoteAudioFrameCount
*Instance Property*
收到遠端的 PCM frame 數量
```swift
@Published var remoteAudioFrameCount: Int { get set }
```
---
# shared
[RevueSDK](../../index.md) › [AudioManager](../index.md) › shared
*Type Property*
Singleton
```swift
static let shared: AudioManager
```
---
# standardChannelCount
[RevueSDK](../../index.md) › [AudioManager](../index.md) › standardChannelCount
*Type Property*
SDK 標準聲道數(1 = mono)
```swift
static let standardChannelCount: Int
```
---
# standardFormat
[RevueSDK](../../index.md) › [AudioManager](../index.md) › standardFormat
*Type Property*
SDK 標準音訊格式(Float32, 24000Hz, 1ch mono)
```swift
static let standardFormat: AVAudioFormat
```
## Discussion
所有注入 SDK 的音訊資料(`AudioBufferCapturer.capture()`) 應符合此格式,否則 SDK 內部會自動轉換(可能影響效能)。
```swift
// 外部注入音訊時,建議先轉換到此格式:
let format = AudioManager.standardFormat
let converter = AVAudioConverter(from: yourFormat, to: format)
```
---
# standardSampleRate
[RevueSDK](../../index.md) › [AudioManager](../index.md) › standardSampleRate
*Type Property*
SDK 標準音訊取樣率(24000 Hz)
```swift
static let standardSampleRate: Double
```
---
# encoding
[RevueSDK](../../index.md) › [AudioPublishOptions](../index.md) › encoding
*Instance Property*
音訊編碼設定
```swift
let encoding: AudioEncoding?
```
---
# AudioPublishOptions
[RevueSDK](../index.md) › AudioPublishOptions
*Structure*
音訊發佈選項 — 設定音訊 Track 的編碼參數
```swift
struct AudioPublishOptions
```
## Overview
```swift
let options = AudioPublishOptions(encoding: AudioEncoding(maxBitrate: 64_000))
```
## Topics
### Initializers
- [`init(encoding: AudioEncoding?)`](init%28encoding:%29/index.md)
### Instance Properties
- [`let encoding: AudioEncoding?`](encoding/index.md)
音訊編碼設定
---
# init(encoding:)
[RevueSDK](../../index.md) › [AudioPublishOptions](../index.md) › init(encoding:)
*Initializer*
```swift
init(encoding: AudioEncoding? = nil)
```
---
# AudioRenderer
[RevueSDK](../index.md) › AudioRenderer
*Protocol*
音訊渲染器協定 — 接收 Float32 PCM 音訊
```swift
protocol AudioRenderer : AnyObject
```
## Overview
外部開發者擴充點,用於進階場景(音量可視化、自訂處理等)。 SDK 內部消費者(AudioMixer、SpeakerPlayer)不透過 renderer,走直接路徑。
**與 `Participant.audioLevel` 的分工**:想做「誰在講話」的 UI 指示直接讀 `participant.isSpeaking` / `audioLevel`(SDK 已內建判定:400ms 窗口 + 40% 佔比 + 平滑), 不需自己接 renderer 算。此協定是給**需要原始 PCM** 的場景(自訂波形視覺化、 語音辨識、自訂訊號處理等)。
## Topics
### Instance Methods
- [`func render(pcmBuffer: AVAudioPCMBuffer)`](render%28pcmbuffer:%29/index.md)
---
# render(pcmBuffer:)
[RevueSDK](../../index.md) › [AudioRenderer](../index.md) › render(pcmBuffer:)
*Instance Method*
```swift
func render(pcmBuffer: AVAudioPCMBuffer)
```
---
# add(audioRenderer:)
[RevueSDK](../../index.md) › [AudioTrack](../index.md) › add(audioRenderer:)
*Instance Method*
```swift
func add(audioRenderer: AudioRenderer)
```
---
# AudioTrack
[RevueSDK](../index.md) › AudioTrack
*Class*
音訊 Track 中間類別 — 統一管理 AudioRenderer 分發
```swift
class AudioTrack
```
## Overview
LocalAudioTrack 和 RemoteAudioTrack 繼承此類別, 不再各自維護 renderer 清單。
## Topics
### Instance Methods
- [`func add(audioRenderer: AudioRenderer)`](add%28audiorenderer:%29/index.md)
- [`func remove(audioRenderer: AudioRenderer)`](remove%28audiorenderer:%29/index.md)
## Relationships
### Inherited By
- [`RemoteAudioTrack`](../remoteaudiotrack/index.md)
- [`LocalAudioTrack`](../localaudiotrack/index.md)
### Inherits From
- [`Track`](../track/index.md)
---
# remove(audioRenderer:)
[RevueSDK](../../index.md) › [AudioTrack](../index.md) › remove(audioRenderer:)
*Instance Method*
```swift
func remove(audioRenderer: AudioRenderer)
```
---
# delegate
[RevueSDK](../../index.md) › [BroadcastManager](../index.md) › delegate
*Instance Property*
- iOS 12.0+
- iPadOS 12.0+
- macCatalyst 12.0+
```swift
var delegate: BroadcastManagerDelegate? { get set }
```
---
# BroadcastManager
[RevueSDK](../index.md) › BroadcastManager
*Class*
Manages the broadcast state and track publication for screen sharing on iOS.
- iOS 12.0+
- iPadOS 12.0+
- macCatalyst 12.0+
```swift
final class BroadcastManager
```
## Topics
### Instance Properties
- [`var delegate: BroadcastManagerDelegate?`](delegate/index.md)
- [`var isBroadcasting: Bool`](isbroadcasting/index.md)
- [`var isBroadcastingPublisher: AnyPublisher`](isbroadcastingpublisher/index.md)
- [`var shouldPublishTrack: Bool`](shouldpublishtrack/index.md)
### Instance Methods
- [`func requestActivation()`](requestactivation%28%29/index.md)
Displays the system broadcast picker, allowing the user to start the broadcast.
- [`func requestStop()`](requeststop%28%29/index.md)
Requests to stop the broadcast.
### Type Properties
- [`static let shared: BroadcastManager`](shared/index.md)
---
# isBroadcasting
[RevueSDK](../../index.md) › [BroadcastManager](../index.md) › isBroadcasting
*Instance Property*
- iOS 12.0+
- iPadOS 12.0+
- macCatalyst 12.0+
```swift
var isBroadcasting: Bool { get }
```
---
# isBroadcastingPublisher
[RevueSDK](../../index.md) › [BroadcastManager](../index.md) › isBroadcastingPublisher
*Instance Property*
- iOS 12.0+
- iPadOS 12.0+
- macCatalyst 12.0+
```swift
var isBroadcastingPublisher: AnyPublisher { get }
```
---
# requestActivation()
[RevueSDK](../../index.md) › [BroadcastManager](../index.md) › requestActivation()
*Instance Method*
Displays the system broadcast picker, allowing the user to start the broadcast.
- iOS 12.0+
- iPadOS 12.0+
- macCatalyst 12.0+
```swift
func requestActivation()
```
## Discussion
> **Note**
>
> This is merely a request and does not guarantee the user will choose to start the broadcast.
---
# requestStop()
[RevueSDK](../../index.md) › [BroadcastManager](../index.md) › requestStop()
*Instance Method*
Requests to stop the broadcast.
- iOS 12.0+
- iPadOS 12.0+
- macCatalyst 12.0+
```swift
func requestStop()
```
---
# shared
[RevueSDK](../../index.md) › [BroadcastManager](../index.md) › shared
*Type Property*
- iOS 12.0+
- iPadOS 12.0+
- macCatalyst 12.0+
```swift
static let shared: BroadcastManager
```
---
# shouldPublishTrack
[RevueSDK](../../index.md) › [BroadcastManager](../index.md) › shouldPublishTrack
*Instance Property*
- iOS 12.0+
- iPadOS 12.0+
- macCatalyst 12.0+
```swift
var shouldPublishTrack: Bool { get set }
```
---
# broadcastManager(didChangeState:)
[RevueSDK](../../index.md) › [BroadcastManagerDelegate](../index.md) › broadcastManager(didChangeState:)
*Instance Method*
Invoked when the broadcast state changes.
```swift
func broadcastManager(didChangeState isBroadcasting: Bool)
```
---
# BroadcastManagerDelegate
[RevueSDK](../index.md) › BroadcastManagerDelegate
*Protocol*
A delegate protocol for receiving updates about the broadcast state.
```swift
@objc protocol BroadcastManagerDelegate : Sendable
```
## Topics
### Instance Methods
- [`func broadcastManager(didChangeState: Bool)`](broadcastmanager%28didchangestate:%29/index.md)
Invoked when the broadcast state changes.
---
# capture(_:)
[RevueSDK](../../index.md) › [BufferCapturer](../index.md) › capture(_:)
*Instance Method*
注入一幀視訊
```swift
func capture(_ pixelBuffer: CVPixelBuffer)
```
## Parameters
- pixelBuffer
要注入的 CVPixelBuffer(其尺寸即為送出解析度)
## Discussion
接受 NV12(`420YpCbCr8BiPlanarVideoRange` / `FullRange`)或 BGRA(`32BGRA`)。 BGRA 來源(例:Flutter RGBA→BGRA 截圖、Metal render 輸出)會自動 BGRA→NV12, 確保下游 `nv12ToToxVideoFrame` → `sendVideoFrame` 路徑能正常解析。
⚠️ **不提供解析度切換功能**:本方法只做格式正規化(BGRA→NV12),**尺寸原樣直通** —— 送出去的解析度 = 你注入的 `pixelBuffer` 的實際尺寸。 **若要動態調整解析度,請自行在注入前調整 input pixelBuffer 的尺寸** (例如自己 render 到目標尺寸再餵進來)。SDK 刻意不代勞:BufferTrack 是外部餵入決定 尺寸的透明管道,且下游(發送/顯示/錄影/合成)皆能自適應任意尺寸。
可在任何執行緒呼叫;尺寸變更事件仍在 main 觸發。
---
# capture(_:timeStampNs:rotation:)
[RevueSDK](../../index.md) › [BufferCapturer](../index.md) › capture(_:timeStampNs:rotation:)
*Instance Method*
注入一幀視訊(帶時間戳和旋轉)
```swift
func capture(_ pixelBuffer: CVPixelBuffer, timeStampNs: Int64, rotation: VideoRotation)
```
## Parameters
- pixelBuffer
要注入的 CVPixelBuffer
- timeStampNs
這一幀的時間戳(奈秒);目前忽略,送出時間以 SDK 收到的時間為準(#152)
- rotation
這一幀要順時針轉幾度才是正的
## Discussion
`rotation` 是這一幀要順時針轉幾度才是正的(同 Broadcast 螢幕分享的轉正): SDK 先把像素轉正(轉成 NV12)再送, 所以 `._90`/`._270` 送出的寬高對調(注入 1280×720 送出 720×1280)。 轉正在 `LocalVideoTrack.processor` 之前,processor 拿到的是轉正後的幀。 其餘行為同 `capture(_:)`。
---
# BufferCapturer
[RevueSDK](../index.md) › BufferCapturer
*Class*
Buffer 擷取器 — 讓 App 注入任意 CVPixelBuffer 作為視訊來源
```swift
class BufferCapturer
```
## Overview
```swift
let track = LocalVideoTrack.createBufferTrack()
let capturer = track.capturer as! BufferCapturer
try await capturer.startCapture() // 沒啟動時注入的畫面都會被丟掉
capturer.capture(myPixelBuffer) // 注入畫面
```
## Topics
### Instance Methods
- [`func capture(CVPixelBuffer)`](capture%28_:%29/index.md)
注入一幀視訊
- [`func capture(CVPixelBuffer, timeStampNs: Int64, rotation: VideoRotation)`](capture%28_:timestampns:rotation:%29/index.md)
注入一幀視訊(帶時間戳和旋轉)
- [`func startCapture() async throws`](startcapture%28%29/index.md)
- [`func stopCapture() async throws`](stopcapture%28%29/index.md)
## Relationships
### Inherits From
- [`VideoCapturer`](../videocapturer/index.md)
---
# startCapture()
[RevueSDK](../../index.md) › [BufferCapturer](../index.md) › startCapture()
*Instance Method*
```swift
override func startCapture() async throws
```
---
# stopCapture()
[RevueSDK](../../index.md) › [BufferCapturer](../index.md) › stopCapture()
*Instance Method*
```swift
override func stopCapture() async throws
```
---
# autoCorrectOrientation
[RevueSDK](../../index.md) › [CameraCaptureOptions](../index.md) › autoCorrectOrientation
*Instance Property*
是否自動校正採集方向(預設 `true`)
```swift
let autoCorrectOrientation: Bool
```
## Discussion
啟用時 SDK 內部自行監聽設備轉向並即時校正相機輸出方向, **App 無須在 AppDelegate 註冊任何方向通知**。 關閉後方向凍結於當前值,適用於 App 自行鎖定畫面方向的場景。
旋轉角度供接收端套用,故其方向開關落在顯示端 `VideoView`;本 SDK 於 **發送端直接轉正像素**(revuekit-c 管線無旋轉 metadata 通道),屬採集層 行為,因此併入本 options。
---
# copyWith(dimensions:fps:position:device:autoCorrectOrientation:isMirrored:)
[RevueSDK](../../index.md) › [CameraCaptureOptions](../index.md) › copyWith(dimensions:fps:position:device:autoCorrectOrientation:isMirrored:)
*Instance Method*
複製並覆蓋部分選項
```swift
func copyWith(dimensions: Dimensions? = nil, fps: Int? = nil, position: AVCaptureDevice.Position? = nil, device: AVCaptureDevice?? = nil, autoCorrectOrientation: Bool? = nil, isMirrored: Bool? = nil) -> CameraCaptureOptions
```
## Parameters
- device
雙層 optional 解 nil 二義性 —— **不傳(`nil`)= 保留原 device**; 傳 `.some(nil)`(即 `.some(Optional.none)`)= 明確清除 device 指定, 改回依 `position` 自動挑選。想清除時:`copyWith(device: .some(nil))`。
## Discussion
建立新 CameraCaptureOptions、未指定的屬性保留原值。
---
# device
[RevueSDK](../../index.md) › [CameraCaptureOptions](../index.md) › device
*Instance Property*
明確指定某一顆相機(可選)
```swift
let device: AVCaptureDevice?
```
## Discussion
- **優先於 `position`**:若同時設定 `device` 與 `position`,以 `device` 為準
- 預設 nil:讓 SDK 依 `position` 自動挑選
- 可搭配 `CameraCapturer.captureDevices()` 列出可用相機後指定
---
# dimensions
[RevueSDK](../../index.md) › [CameraCaptureOptions](../index.md) › dimensions
*Instance Property*
相機採集解析度(預設 720p 16:9)
```swift
let dimensions: Dimensions
```
## Discussion
相機若不支援指定解析度,CameraCapturer 內部會 fallback 到最接近的支援值。
---
# !=(_:_:)
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › Equatable Implementations
*Article*
---
# h1080_169
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h1080_169
*Type Property*
```swift
static let h1080_169: CameraCaptureOptions.Dimensions
```
---
# h1080_43
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h1080_43
*Type Property*
```swift
static let h1080_43: CameraCaptureOptions.Dimensions
```
---
# h120_43
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h120_43
*Type Property*
```swift
static let h120_43: CameraCaptureOptions.Dimensions
```
---
# h1440_169
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h1440_169
*Type Property*
```swift
static let h1440_169: CameraCaptureOptions.Dimensions
```
---
# h1440_43
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h1440_43
*Type Property*
```swift
static let h1440_43: CameraCaptureOptions.Dimensions
```
---
# h180_169
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h180_169
*Type Property*
```swift
static let h180_169: CameraCaptureOptions.Dimensions
```
---
# h180_43
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h180_43
*Type Property*
```swift
static let h180_43: CameraCaptureOptions.Dimensions
```
---
# h2160_169
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h2160_169
*Type Property*
```swift
static let h2160_169: CameraCaptureOptions.Dimensions
```
---
# h216_169
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h216_169
*Type Property*
```swift
static let h216_169: CameraCaptureOptions.Dimensions
```
---
# h240_43
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h240_43
*Type Property*
```swift
static let h240_43: CameraCaptureOptions.Dimensions
```
---
# h360_169
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h360_169
*Type Property*
```swift
static let h360_169: CameraCaptureOptions.Dimensions
```
---
# h360_43
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h360_43
*Type Property*
```swift
static let h360_43: CameraCaptureOptions.Dimensions
```
---
# h480_43
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h480_43
*Type Property*
```swift
static let h480_43: CameraCaptureOptions.Dimensions
```
---
# h540_169
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h540_169
*Type Property*
```swift
static let h540_169: CameraCaptureOptions.Dimensions
```
---
# h540_43
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h540_43
*Type Property*
```swift
static let h540_43: CameraCaptureOptions.Dimensions
```
---
# h720_169
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h720_169
*Type Property*
```swift
static let h720_169: CameraCaptureOptions.Dimensions
```
---
# h720_43
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h720_43
*Type Property*
```swift
static let h720_43: CameraCaptureOptions.Dimensions
```
---
# h90_169
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › h90_169
*Type Property*
```swift
static let h90_169: CameraCaptureOptions.Dimensions
```
---
# height
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › height
*Instance Property*
```swift
let height: Int32
```
---
# CameraCaptureOptions.Dimensions
[RevueSDK](../../index.md) › [CameraCaptureOptions](../index.md) › CameraCaptureOptions.Dimensions
*Structure*
相機採集解析度
```swift
struct Dimensions
```
## Overview
也可透過 `Dimensions(width:height:)` 自訂任意尺寸;相機不支援時內部會 fallback 到最接近的支援值。
## Topics
### Initializers
- [`init(width: Int32, height: Int32)`](init%28width:height:%29/index.md)
### Instance Properties
- [`let height: Int32`](height/index.md)
- [`let width: Int32`](width/index.md)
### Type Properties
- [`static let h1080_169: CameraCaptureOptions.Dimensions`](h1080_169/index.md)
- [`static let h1080_43: CameraCaptureOptions.Dimensions`](h1080_43/index.md)
- [`static let h120_43: CameraCaptureOptions.Dimensions`](h120_43/index.md)
- [`static let h1440_169: CameraCaptureOptions.Dimensions`](h1440_169/index.md)
- [`static let h1440_43: CameraCaptureOptions.Dimensions`](h1440_43/index.md)
- [`static let h180_169: CameraCaptureOptions.Dimensions`](h180_169/index.md)
- [`static let h180_43: CameraCaptureOptions.Dimensions`](h180_43/index.md)
- [`static let h2160_169: CameraCaptureOptions.Dimensions`](h2160_169/index.md)
- [`static let h216_169: CameraCaptureOptions.Dimensions`](h216_169/index.md)
- [`static let h240_43: CameraCaptureOptions.Dimensions`](h240_43/index.md)
- [`static let h360_169: CameraCaptureOptions.Dimensions`](h360_169/index.md)
- [`static let h360_43: CameraCaptureOptions.Dimensions`](h360_43/index.md)
- [`static let h480_43: CameraCaptureOptions.Dimensions`](h480_43/index.md)
- [`static let h540_169: CameraCaptureOptions.Dimensions`](h540_169/index.md)
- [`static let h540_43: CameraCaptureOptions.Dimensions`](h540_43/index.md)
- [`static let h720_169: CameraCaptureOptions.Dimensions`](h720_169/index.md)
- [`static let h720_43: CameraCaptureOptions.Dimensions`](h720_43/index.md)
- [`static let h90_169: CameraCaptureOptions.Dimensions`](h90_169/index.md)
---
# init(width:height:)
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › init(width:height:)
*Initializer*
```swift
init(width: Int32, height: Int32)
```
---
# width
[RevueSDK](../../../index.md) › [CameraCaptureOptions](../../index.md) › [CameraCaptureOptions.Dimensions](../index.md) › width
*Instance Property*
```swift
let width: Int32
```
---
# fps
[RevueSDK](../../index.md) › [CameraCaptureOptions](../index.md) › fps
*Instance Property*
相機採集幀率 — 每秒送出幾張畫面(預設 30)
```swift
let fps: Int
```
## Discussion
- 常見值:15 / 24 / 30 / 60
- **一般建議最高 30**:SDK 不硬性設限,但**不建議設定超過 30**。 內部降頻 Timer 會照設定值定頻取幀;當設定值高於相機實際吐幀速率時, Timer 會重複送出同一張畫面(畫面不會更順),只是白白增加 GPU render 與發送負擔、影響效能與耗電。有特殊需求(如高更新率場景)才調高。
- **跟 `VideoPublishOptions.maxFps` 分離**:前者為採集,後者為編碼發送節流
---
# CameraCaptureOptions
[RevueSDK](../index.md) › CameraCaptureOptions
*Class*
相機擷取選項
```swift
nonisolated class CameraCaptureOptions
```
## Overview
描述「相機硬體要以什麼規格採集畫面」— 決定相機吐出的 frame 尺寸、幀率、 使用哪一顆鏡頭。此為**採集層**設定,跟編碼層 (`VideoPublishOptions`) 分離:
- `CameraCaptureOptions.fps` = 相機硬體吐幀速率(AVCaptureDevice 採樣頻率)
- `VideoPublishOptions.maxFps` = 編碼器發送端節流上限(軟體丟幀)
兩者可各自設定:capture 30fps 拍、encode 只送 15fps 出去也合法。
```swift
let options = CameraCaptureOptions(
dimensions: .h720_169,
fps: 15,
position: .front
)
let track = localParticipant.setCamera(enabled: true, options: options)
```
`CameraCapturer.set(options:)`。
## Topics
### Structures
- [`struct Dimensions`](dimensions-swift.struct/index.md)
相機採集解析度
### Initializers
- [`init(dimensions: Dimensions, fps: Int, position: AVCaptureDevice.Position, device: AVCaptureDevice?, autoCorrectOrientation: Bool, isMirrored: Bool)`](init%28dimensions:fps:position:device:autocorrectorientation:ismirrored:%29/index.md)
### Instance Properties
- [`let autoCorrectOrientation: Bool`](autocorrectorientation/index.md)
是否自動校正採集方向(預設 `true`)
- [`let device: AVCaptureDevice?`](device/index.md)
明確指定某一顆相機(可選)
- [`let dimensions: Dimensions`](dimensions-swift.property/index.md)
相機採集解析度(預設 720p 16:9)
- [`let fps: Int`](fps/index.md)
相機採集幀率 — 每秒送出幾張畫面(預設 30)
- [`let isMirrored: Bool`](ismirrored/index.md)
是否鏡像採集(預設 `false`)
- [`let position: AVCaptureDevice.Position`](position/index.md)
鏡頭位置偏好(預設 `.front`)
### Instance Methods
- [`func copyWith(dimensions: Dimensions?, fps: Int?, position: AVCaptureDevice.Position?, device: AVCaptureDevice??, autoCorrectOrientation: Bool?, isMirrored: Bool?) -> CameraCaptureOptions`](copywith%28dimensions:fps:position:device:autocorrectorientation:ismirrored:%29/index.md)
複製並覆蓋部分選項
---
# init(dimensions:fps:position:device:autoCorrectOrientation:isMirrored:)
[RevueSDK](../../index.md) › [CameraCaptureOptions](../index.md) › init(dimensions:fps:position:device:autoCorrectOrientation:isMirrored:)
*Initializer*
```swift
init(dimensions: Dimensions = .h720_169, fps: Int = 30, position: AVCaptureDevice.Position = .front, device: AVCaptureDevice? = nil, autoCorrectOrientation: Bool = true, isMirrored: Bool = false)
```
---
# isMirrored
[RevueSDK](../../index.md) › [CameraCaptureOptions](../index.md) › isMirrored
*Instance Property*
是否鏡像採集(預設 `false`)
```swift
let isMirrored: Bool
```
## Discussion
> **Important**
>
> **這是採集來源本身的鏡像,不只是本地顯示。** 相機輸出的畫面即為鏡像,因此**同時影響本地預覽與發送給對方的畫面** —— 對端收到、以及錄影 / 合成的結果也都會是鏡像的。
>
> 若只想讓「自己看到鏡像、對方看到正常」(視訊通話的一般慣例), 請改用顯示端鏡像 `VideoView.mirrorMode`,不要開啟本選項。 本選項的用途是「來源即鏡像」——適用於無法使用 `VideoView` 的宿主 (如 Flutter texture 直接取用 pixel buffer)而仍需雙方一致鏡像的場景。
> **Note**
>
> **不分前後鏡頭**,開啟即對當前鏡頭生效。SDK 為能力層, 「哪顆鏡頭該鏡像」屬應用層決策 —— 需要只鏡像前鏡頭時,由應用層在 切換鏡頭時自行帶入對應的 `isMirrored`。
---
# position
[RevueSDK](../../index.md) › [CameraCaptureOptions](../index.md) › position
*Instance Property*
鏡頭位置偏好(預設 `.front`)
```swift
let position: AVCaptureDevice.Position
```
## Discussion
- `.front`:前鏡頭(視訊會議首選)
- `.back`:後鏡頭
- `.unspecified`:讓 SDK 依裝置能力挑選
通話中要切換前後鏡頭,**不要**重建 options,改用:
- `CameraCapturer.switchCameraPosition()` — 前後互切
- `CameraCapturer.set(cameraPosition:)` — 明確指定
---
# canSwitchPosition()
[RevueSDK](../../index.md) › [CameraCapturer](../index.md) › canSwitchPosition()
*Type Method*
是否可以切換鏡頭位置
```swift
static func canSwitchPosition() -> Bool
```
---
# captureDevices()
[RevueSDK](../../index.md) › [CameraCapturer](../index.md) › captureDevices()
*Type Method*
列出所有可用相機裝置
```swift
static func captureDevices() -> [AVCaptureDevice]
```
---
# captureSession
[RevueSDK](../../index.md) › [CameraCapturer](../index.md) › captureSession
*Instance Property*
SDK 內部擷取用的 `AVCaptureSession`(唯讀轉發)
```swift
var captureSession: AVCaptureSession { get }
```
## Discussion
只能讀,不要自己加 input/output:SDK 換鏡頭、改解析度時會重設 input 與格式。
---
# device
[RevueSDK](../../index.md) › [CameraCapturer](../index.md) › device
*Instance Property*
使用中鏡頭:SDK 實際正在擷取的那一顆
```swift
var device: AVCaptureDevice? { get }
```
## Discussion
啟動成功、切換成功後更新成實際開著的那顆;停止後是 nil。 和 `options.device`(指定鏡頭)分開:走 `position` 自動挑選時 `options.device` 是 nil, 這裡仍讀得到鏡頭;切換失敗還原時這裡仍是舊的那顆。
只在 main 寫入(切換鏡頭在擷取佇列完成,切到 main 再寫)。
---
# CameraCapturer
[RevueSDK](../index.md) › CameraCapturer
*Class*
相機擷取器 — 控制相機硬體
```swift
class CameraCapturer
```
## Overview
透過 `LocalVideoTrack.capturer as? CameraCapturer` 取得。
```swift
let capturer = localParticipant.cameraTrack.capturer as! CameraCapturer
try await capturer.switchCameraPosition() // 切換前後鏡頭
try await capturer.set(options: newOptions) // 即時調整 FPS/解析度
```
## Topics
### Instance Properties
- [`var captureSession: AVCaptureSession`](capturesession/index.md)
SDK 內部擷取用的 `AVCaptureSession`(唯讀轉發)
- [`var device: AVCaptureDevice?`](device/index.md)
使用中鏡頭:SDK 實際正在擷取的那一顆
- [`var options: CameraCaptureOptions`](options/index.md)
當前擷取選項
- [`var position: AVCaptureDevice.Position`](position/index.md)
當前鏡頭位置
### Instance Methods
- [`func restartCapture() async throws`](restartcapture%28%29/index.md)
重啟擷取(選項變更後)
- [`func set(cameraPosition: AVCaptureDevice.Position) async throws -> Bool`](set%28cameraposition:%29/index.md)
指定鏡頭位置
- [`func set(options: CameraCaptureOptions) async throws -> Bool`](set%28options:%29/index.md)
即時更新擷取選項(解析度、FPS、鏡頭)
- [`func startCapture() async throws`](startcapture%28%29/index.md)
啟動擷取
- [`func stopCapture() async throws`](stopcapture%28%29/index.md)
停止擷取
- [`func switchCameraPosition() async throws -> Bool`](switchcameraposition%28%29/index.md)
切換前後鏡頭
### Type Methods
- [`static func canSwitchPosition() -> Bool`](canswitchposition%28%29/index.md)
是否可以切換鏡頭位置
- [`static func captureDevices() -> [AVCaptureDevice]`](capturedevices%28%29/index.md)
列出所有可用相機裝置
## Relationships
### Inherits From
- [`VideoCapturer`](../videocapturer/index.md)
---
# options
[RevueSDK](../../index.md) › [CameraCapturer](../index.md) › options
*Instance Property*
當前擷取選項
```swift
var options: CameraCaptureOptions { get }
```
---
# position
[RevueSDK](../../index.md) › [CameraCapturer](../index.md) › position
*Instance Property*
當前鏡頭位置
```swift
var position: AVCaptureDevice.Position { get }
```
---
# restartCapture()
[RevueSDK](../../index.md) › [CameraCapturer](../index.md) › restartCapture()
*Instance Method*
重啟擷取(選項變更後)
```swift
func restartCapture() async throws
```
---
# set(cameraPosition:)
[RevueSDK](../../index.md) › [CameraCapturer](../index.md) › set(cameraPosition:)
*Instance Method*
指定鏡頭位置
```swift
@discardableResult func set(cameraPosition: AVCaptureDevice.Position) async throws -> Bool
```
## Return Value
同 `switchCameraPosition()`
## Discussion
行為同 `switchCameraPosition()`(清除 `options.device`、加不進去會還原)。
> **Throws**
>
> `CameraCapturerError.deviceNotAvailable` —— 該方向沒有鏡頭;`position` 不變
---
# set(options:)
[RevueSDK](../../index.md) › [CameraCapturer](../index.md) › set(options:)
*Instance Method*
即時更新擷取選項(解析度、FPS、鏡頭)
```swift
@discardableResult func set(options: CameraCaptureOptions) async throws -> Bool
```
## Parameters
- options
新的擷取選項
## Return Value
`true` —— 擷取中,且新選項已生效(新解析度已套用;有換鏡頭時要的那顆已換上)。 `false` —— 鏡頭停止中(只記住,`startCapture()` 時套用)、換鏡頭加不進去已還原, 或還沒換就被之後的呼叫取代(同 `switchCameraPosition()`)
## Discussion
`CameraCaptureOptions` 的欄位都是 `let`:要改設定就 `copyWith` 後傳進來。
> **Throws**
>
> `CameraCapturerError.deviceNotAvailable` —— `options.position` 那個方向沒有鏡頭, 或擷取中 `options.device` 那顆建不出 input(例如已拔除;停止中只記住,`startCapture()` 開不起來時改用 `position` 那顆);丟錯時 `options` 與 `position` 都不變
```swift
try await capturer.set(options: capturer.options.copyWith(
dimensions: .h1080_169,
fps: 15
))
```
---
# startCapture()
[RevueSDK](../../index.md) › [CameraCapturer](../index.md) › startCapture()
*Instance Method*
啟動擷取
```swift
override func startCapture() async throws
```
---
# stopCapture()
[RevueSDK](../../index.md) › [CameraCapturer](../index.md) › stopCapture()
*Instance Method*
停止擷取
```swift
override func stopCapture() async throws
```
---
# switchCameraPosition()
[RevueSDK](../../index.md) › [CameraCapturer](../index.md) › switchCameraPosition()
*Instance Method*
切換前後鏡頭
```swift
@discardableResult func switchCameraPosition() async throws -> Bool
```
## Return Value
`true` —— 擷取中,且要的那顆鏡頭已換上(`device` 已是它)。 `false` —— 沒換上:鏡頭停止中(只記住,`startCapture()` 時套用)、加不進去已還原, 或還沒換就被之後的呼叫取代(連續呼叫只執行最後一次,前面的都回 `false`)
## Discussion
切前後鏡頭 = 回「依 position 自動挑選」,清除先前指定的 `options.device`。 擷取中才即時切換;停止中只記住 position,`startCapture()` 時套用。
鏡頭在擷取佇列裡加不進去時(例如被其他 App 占用),SDK 還原成原本的鏡頭、 `position` 與 `options` 回到原值,不丟錯誤,回 `false`。
> **Throws**
>
> `CameraCapturerError.deviceNotAvailable` —— 另一個方向沒有鏡頭;`position` 不變
---
# !=(_:_:)
[RevueSDK](../../index.md) › [CameraCapturerError](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# CameraCapturerError.deviceNotAvailable
[RevueSDK](../../index.md) › [CameraCapturerError](../index.md) › CameraCapturerError.deviceNotAvailable
*Case*
要的方向沒有鏡頭(或鏡頭無法使用)
```swift
case deviceNotAvailable
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [CameraCapturerError](../index.md) › Equatable Implementations
*Article*
---
# Error Implementations
[RevueSDK](../../index.md) › [CameraCapturerError](../index.md) › Error Implementations
*Article*
---
# errorDescription
[RevueSDK](../../index.md) › [CameraCapturerError](../index.md) › errorDescription
*Instance Property*
```swift
var errorDescription: String? { get }
```
---
# failureReason
[RevueSDK](../../index.md) › [CameraCapturerError](../index.md) › failureReason
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var failureReason: String? { get }
```
---
# helpAnchor
[RevueSDK](../../index.md) › [CameraCapturerError](../index.md) › helpAnchor
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var helpAnchor: String? { get }
```
---
# CameraCapturerError
[RevueSDK](../index.md) › CameraCapturerError
*Enumeration*
相機擷取器的錯誤
```swift
enum CameraCapturerError
```
## Overview
`CameraCapturer.switchCameraPosition()`、`set(cameraPosition:)`、`set(options:)` 與 `startCapture()` 會丟出。
## Topics
### Enumeration Cases
- [`case deviceNotAvailable`](devicenotavailable/index.md)
要的方向沒有鏡頭(或鏡頭無法使用)
### Instance Properties
- [`var errorDescription: String?`](errordescription/index.md)
---
# localizedDescription
[RevueSDK](../../index.md) › [CameraCapturerError](../index.md) › localizedDescription
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var localizedDescription: String { get }
```
---
# LocalizedError Implementations
[RevueSDK](../../index.md) › [CameraCapturerError](../index.md) › LocalizedError Implementations
*Article*
---
# recoverySuggestion
[RevueSDK](../../index.md) › [CameraCapturerError](../index.md) › recoverySuggestion
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var recoverySuggestion: String? { get }
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [CaptureState](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# CaptureState.capturing
[RevueSDK](../../index.md) › [CaptureState](../index.md) › CaptureState.capturing
*Case*
```swift
case capturing
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [CaptureState](../index.md) › Equatable Implementations
*Article*
---
# CaptureState.error
[RevueSDK](../../index.md) › [CaptureState](../index.md) › CaptureState.error
*Case*
```swift
case error
```
---
# hash(into:)
[RevueSDK](../../index.md) › [CaptureState](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [CaptureState](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# CaptureState.idle
[RevueSDK](../../index.md) › [CaptureState](../index.md) › CaptureState.idle
*Case*
```swift
case idle
```
---
# CaptureState
[RevueSDK](../index.md) › CaptureState
*Enumeration*
擷取器狀態
```swift
enum CaptureState
```
## Overview
螢幕分享需區分「正常停止」與「ReplayKit 啟動失敗/使用者取消授權」, 收斂成兩態會弄丟該資訊。
## Topics
### Enumeration Cases
- [`case capturing`](capturing/index.md)
- [`case error`](error/index.md)
- [`case idle`](idle/index.md)
### Initializers
- [`init?(rawValue: Int)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../index.md) › [CaptureState](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [CaptureState](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [CaptureState](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [CaptureState](../index.md) › RawRepresentable Implementations
*Article*
---
# !=(_:_:)
[RevueSDK](../../index.md) › [CodecStatistics](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [CodecStatistics](../index.md) › Equatable Implementations
*Article*
---
# CodecStatistics
[RevueSDK](../index.md) › CodecStatistics
*Structure*
當前視訊編碼統計。
```swift
struct CodecStatistics
```
## Topics
### Instance Properties
- [`let mimeType: String`](mimetype/index.md)
當前實際視訊編碼(“video/VP8” / “video/H264”)。 診斷:確認 codec 切換是否生效——以為切 H264 卻顯示 VP8 = 切換漏生效。 來源:依「最近一幀走哪條 receive callback」判定——raw callback=VP8、encoded callback=H264。 ⚠️ 不用 get_negotiated_video_codec(那是能力涵蓋值,回 h264 代表 h264+vp8 皆可、非當前實際)。
---
# mimeType
[RevueSDK](../../index.md) › [CodecStatistics](../index.md) › mimeType
*Instance Property*
當前實際視訊編碼(“video/VP8” / “video/H264”)。 診斷:確認 codec 切換是否生效——以為切 H264 卻顯示 VP8 = 切換漏生效。 來源:依「最近一幀走哪條 receive callback」判定——raw callback=VP8、encoded callback=H264。 ⚠️ 不用 get_negotiated_video_codec(那是能力涵蓋值,回 h264 代表 h264+vp8 皆可、非當前實際)。
```swift
let mimeType: String
```
---
# CompositeTemplate
[RevueSDK](../index.md) › CompositeTemplate
*Structure*
合成模板 — 定義多路 VideoTrack 的排版方式
```swift
struct CompositeTemplate
```
## Overview
模板只管排版(Track 位置),不管解析度。 解析度由 `VideoTrackCompositor.resolutionSize` 控制。
```swift
let template = CompositeTemplate(trackPositions: [
VideoTrackPositionInfo(track: cameraTrack, normalizedRect: CGRect(x: 0, y: 0, width: 0.5, height: 0.5)),
VideoTrackPositionInfo(track: remoteTrackA, normalizedRect: CGRect(x: 0.5, y: 0, width: 0.5, height: 0.5)),
])
room.videoTrackCompositor.setTemplate(template)
```
## Topics
### Initializers
- [`init(trackPositions: [VideoTrackPositionInfo])`](init%28trackpositions:%29/index.md)
### Instance Properties
- [`let trackPositions: [VideoTrackPositionInfo]`](trackpositions/index.md)
Track 位置資訊陣列(按順序繪製,順序 = 圖層,後畫的蓋住先畫的)
---
# init(trackPositions:)
[RevueSDK](../../index.md) › [CompositeTemplate](../index.md) › init(trackPositions:)
*Initializer*
```swift
init(trackPositions: [VideoTrackPositionInfo])
```
---
# trackPositions
[RevueSDK](../../index.md) › [CompositeTemplate](../index.md) › trackPositions
*Instance Property*
Track 位置資訊陣列(按順序繪製,順序 = 圖層,後畫的蓋住先畫的)
```swift
let trackPositions: [VideoTrackPositionInfo]
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# ConnectionQuality.bad
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › ConnectionQuality.bad
*Case*
極差(score ≤ 25)
```swift
case bad
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › Equatable Implementations
*Article*
---
# ConnectionQuality.excellent
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › ConnectionQuality.excellent
*Case*
極佳(score > 85)
```swift
case excellent
```
---
# ConnectionQuality.fair
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › ConnectionQuality.fair
*Case*
普通(score 45–65)
```swift
case fair
```
---
# ConnectionQuality.good
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › ConnectionQuality.good
*Case*
良好(score 65–85)
```swift
case good
```
---
# hash(into:)
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# ConnectionQuality
[RevueSDK](../index.md) › ConnectionQuality
*Enumeration*
參與者的連線品質(5 級)
```swift
enum ConnectionQuality
```
## Overview
(score = 100 − 延遲扣分 − 丟包扣分)。以下為各級對應的分數區間:
## Topics
### Enumeration Cases
- [`case bad`](bad/index.md)
極差(score ≤ 25)
- [`case excellent`](excellent/index.md)
極佳(score > 85)
- [`case fair`](fair/index.md)
普通(score 45–65)
- [`case good`](good/index.md)
良好(score 65–85)
- [`case poor`](poor/index.md)
較差(score 25–45)
- [`case unknown`](unknown/index.md)
未知(尚無統計資料)
### Initializers
- [`init?(rawValue: Int)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# ConnectionQuality.poor
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › ConnectionQuality.poor
*Case*
較差(score 25–45)
```swift
case poor
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › RawRepresentable Implementations
*Article*
---
# ConnectionQuality.unknown
[RevueSDK](../../index.md) › [ConnectionQuality](../index.md) › ConnectionQuality.unknown
*Case*
未知(尚無統計資料)
```swift
case unknown
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [DegradationPreference](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# DegradationPreference.balanced
[RevueSDK](../../index.md) › [DegradationPreference](../index.md) › DegradationPreference.balanced
*Case*
平衡(同時降畫質和 FPS)
```swift
case balanced
```
---
# DegradationPreference.disabled
[RevueSDK](../../index.md) › [DegradationPreference](../index.md) › DegradationPreference.disabled
*Case*
不降級
```swift
case disabled
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [DegradationPreference](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../index.md) › [DegradationPreference](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [DegradationPreference](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# DegradationPreference
[RevueSDK](../index.md) › DegradationPreference
*Enumeration*
品質降級偏好 — 頻寬不足時優先降什麼
```swift
enum DegradationPreference
```
## Overview
⚠️ **本 enum 目前保留 API 骨架、內部未實作**
- 使用者塞值不會生效
- 目前實際降級策略由 `Vp8Encoding.rateControlMode`(VBR/CBR/CQ)承擔
- 未來將對接 revuekit-c 的 `resize_allowed` 機制(弱網自動縮小解析度)
## Topics
### Enumeration Cases
- [`case balanced`](balanced/index.md)
平衡(同時降畫質和 FPS)
- [`case disabled`](disabled/index.md)
不降級
- [`case maintainFramerate`](maintainframerate/index.md)
維持流暢(優先降畫質)
- [`case maintainResolution`](maintainresolution/index.md)
維持畫質(優先降 FPS)
### Initializers
- [`init?(rawValue: String)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../index.md) › [DegradationPreference](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: String)
```
---
# DegradationPreference.maintainFramerate
[RevueSDK](../../index.md) › [DegradationPreference](../index.md) › DegradationPreference.maintainFramerate
*Case*
維持流暢(優先降畫質)
```swift
case maintainFramerate
```
---
# DegradationPreference.maintainResolution
[RevueSDK](../../index.md) › [DegradationPreference](../index.md) › DegradationPreference.maintainResolution
*Case*
維持畫質(優先降 FPS)
```swift
case maintainResolution
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [DegradationPreference](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [DegradationPreference](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [DegradationPreference](../index.md) › RawRepresentable Implementations
*Article*
---
# !=(_:_:)
[RevueSDK](../../index.md) › [DisconnectReason](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [DisconnectReason](../index.md) › Equatable Implementations
*Article*
---
# DisconnectReason
[RevueSDK](../index.md) › DisconnectReason
*Enumeration*
通話中對方斷線原因
```swift
enum DisconnectReason
```
## Overview
用於 `RemoteParticipant.lastDisconnectReason`,App 在 `participantDidDisconnect` callback 內可從 participant 屬性讀取,區分對方主動掛斷或網路斷線。
## Topics
### Enumeration Cases
- [`case leave`](leave/index.md)
對方主動離開(ToxAV finished 事件)
- [`case networkLost`](networklost/index.md)
連線錯誤導致斷線(ToxAV error 事件,例如對方網路錯誤或本端重連寬限期到期)
---
# DisconnectReason.leave
[RevueSDK](../../index.md) › [DisconnectReason](../index.md) › DisconnectReason.leave
*Case*
對方主動離開(ToxAV finished 事件)
```swift
case leave
```
---
# DisconnectReason.networkLost
[RevueSDK](../../index.md) › [DisconnectReason](../index.md) › DisconnectReason.networkLost
*Case*
連線錯誤導致斷線(ToxAV error 事件,例如對方網路錯誤或本端重連寬限期到期)
```swift
case networkLost
```
## Discussion
連線路徑全斷(`paths` 變空)本身不會造成斷線:C 層仍在重連,只有一直連不回、 ToxAV 宣告通話結束時才以此原因斷線。
---
# !=(_:_:)
[RevueSDK](../../index.md) › [DiskSpaceWarning](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# DiskSpaceWarning.criticalStopped(available:threshold:)
[RevueSDK](../../index.md) › [DiskSpaceWarning](../index.md) › DiskSpaceWarning.criticalStopped(available:threshold:)
*Case*
可用空間低於臨界閾值,錄影已自動停止
```swift
case criticalStopped(available: Int64, threshold: Int64)
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [DiskSpaceWarning](../index.md) › Equatable Implementations
*Article*
---
# DiskSpaceWarning
[RevueSDK](../index.md) › DiskSpaceWarning
*Enumeration*
磁碟空間警告事件
```swift
enum DiskSpaceWarning
```
## Topics
### Enumeration Cases
- [`case criticalStopped(available: Int64, threshold: Int64)`](criticalstopped%28available:threshold:%29/index.md)
可用空間低於臨界閾值,錄影已自動停止
- [`case warning(available: Int64, threshold: Int64)`](warning%28available:threshold:%29/index.md)
可用空間低於警告閾值,但仍繼續錄影
---
# DiskSpaceWarning.warning(available:threshold:)
[RevueSDK](../../index.md) › [DiskSpaceWarning](../index.md) › DiskSpaceWarning.warning(available:threshold:)
*Case*
可用空間低於警告閾值,但仍繼續錄影
```swift
case warning(available: Int64, threshold: Int64)
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [FrameCryptoError](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# FrameCryptoError.bufferOversize(mediaType:ssrc:)
[RevueSDK](../../index.md) › [FrameCryptoError](../index.md) › FrameCryptoError.bufferOversize(mediaType:ssrc:)
*Case*
加密寫出超過 `getMaxSize` 宣告上限(僅發生在加密方向、無 caller code)
```swift
case bufferOversize(mediaType: FrameMediaType, ssrc: UInt32)
```
---
# FrameCryptoError.decryptFailed(mediaType:ssrc:code:)
[RevueSDK](../../index.md) › [FrameCryptoError](../index.md) › FrameCryptoError.decryptFailed(mediaType:ssrc:code:)
*Case*
解密 callback 回非 0(`code` 為該回傳值)
```swift
case decryptFailed(mediaType: FrameMediaType, ssrc: UInt32, code: Int)
```
---
# FrameCryptoError.encryptFailed(mediaType:ssrc:code:)
[RevueSDK](../../index.md) › [FrameCryptoError](../index.md) › FrameCryptoError.encryptFailed(mediaType:ssrc:code:)
*Case*
加密 callback 回非 0(`code` 為該回傳值)
```swift
case encryptFailed(mediaType: FrameMediaType, ssrc: UInt32, code: Int)
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [FrameCryptoError](../index.md) › Equatable Implementations
*Article*
---
# Error Implementations
[RevueSDK](../../index.md) › [FrameCryptoError](../index.md) › Error Implementations
*Article*
---
# FrameCryptoError
[RevueSDK](../index.md) › FrameCryptoError
*Enumeration*
Frame crypto 加解密逐幀錯誤(v0.14.0,C 層 `REVUEKIT_EVENT_FRAME_CRYPTO_ERROR` type 102)。
```swift
enum FrameCryptoError
```
## Overview
runtime 加解密失敗時,SDK **drop 該幀、不中斷通話**,並透過 `RevueDelegate.didReceiveFrameCryptoError(_:)` 通知。串接端據此觀察錯誤率、 決定是否提示使用者或掛斷。持續出現=持續黑畫面/無聲。
case 表達方向(encrypt/decrypt)與原因(callback 失敗 / buffer 過小), 關聯值帶媒體型別、ssrc 與 caller 回傳碼(對齊既有 `RevueInitError.initFailed(code:)` / `RoomMessageError.messageTooLong(size:maxSize:)` 的關聯值風格)。
## Topics
### Enumeration Cases
- [`case bufferOversize(mediaType: FrameMediaType, ssrc: UInt32)`](bufferoversize%28mediatype:ssrc:%29/index.md)
加密寫出超過 `getMaxSize` 宣告上限(僅發生在加密方向、無 caller code)
- [`case decryptFailed(mediaType: FrameMediaType, ssrc: UInt32, code: Int)`](decryptfailed%28mediatype:ssrc:code:%29/index.md)
解密 callback 回非 0(`code` 為該回傳值)
- [`case encryptFailed(mediaType: FrameMediaType, ssrc: UInt32, code: Int)`](encryptfailed%28mediatype:ssrc:code:%29/index.md)
加密 callback 回非 0(`code` 為該回傳值)
---
# localizedDescription
[RevueSDK](../../index.md) › [FrameCryptoError](../index.md) › localizedDescription
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var localizedDescription: String { get }
```
---
# FrameDecryptCallback
[RevueSDK](../index.md) › FrameDecryptCallback
*Type Alias*
解密單幀 callback。回 0=成功;非 0=失敗(drop 該幀 + 發 `FrameCryptoError` 事件)。
```swift
typealias FrameDecryptCallback = @convention(c) (UInt32, UInt32, UInt64, UnsafePointer?, Int, UnsafeMutablePointer?, Int, UnsafeMutablePointer?, UnsafeMutableRawPointer?) -> Int32
```
## Discussion
參數:`(mediaType, ssrc, counter, ciphertext, ciphertextSize, plaintext, plaintextCapacity, outPlaintextSize, userData)`。解密不會 grow:outPlaintextSize ≤ ciphertextSize。
---
# FrameEncryptCallback
[RevueSDK](../index.md) › FrameEncryptCallback
*Type Alias*
加密單幀 callback。回 0=成功;非 0=失敗(drop 該幀 + 發 `FrameCryptoError` 事件)。
```swift
typealias FrameEncryptCallback = @convention(c) (UInt32, UInt32, UnsafePointer?, Int, UnsafeMutablePointer?, Int, UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafeMutableRawPointer?) -> Int32
```
## Discussion
參數:`(mediaType, ssrc, plaintext, plaintextSize, ciphertext, ciphertextCapacity, outCiphertextSize, outCounter, userData)`。
- 寫密文到 `ciphertext[0..
# FrameGetMaxSizeCallback
[RevueSDK](../index.md) › FrameGetMaxSizeCallback
*Type Alias*
查詢密文上限 callback。對相同 (mediaType, plaintextSize, userData state) 須 deterministic, 回傳值必須 ≥ encrypt 實際寫出的大小(XOR/AES-CTR 回 plaintextSize;AES-GCM 回 plaintextSize+16)。
```swift
typealias FrameGetMaxSizeCallback = @convention(c) (UInt32, Int, UnsafeMutableRawPointer?) -> Int
```
## Discussion
參數:`(mediaType, plaintextSize, userData)`,回傳密文上限(size_t)。
---
# !=(_:_:)
[RevueSDK](../../index.md) › [FrameMediaType](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# FrameMediaType.audio
[RevueSDK](../../index.md) › [FrameMediaType](../index.md) › FrameMediaType.audio
*Case*
```swift
case audio
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [FrameMediaType](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../index.md) › [FrameMediaType](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [FrameMediaType](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# FrameMediaType
[RevueSDK](../index.md) › FrameMediaType
*Enumeration*
Frame 媒體型別
```swift
enum FrameMediaType
```
## Topics
### Enumeration Cases
- [`case audio`](audio/index.md)
- [`case video`](video/index.md)
### Initializers
- [`init?(rawValue: UInt32)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../index.md) › [FrameMediaType](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: UInt32)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [FrameMediaType](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [FrameMediaType](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [FrameMediaType](../index.md) › RawRepresentable Implementations
*Article*
---
# FrameMediaType.video
[RevueSDK](../../index.md) › [FrameMediaType](../index.md) › FrameMediaType.video
*Case*
```swift
case video
```
---
# FriendAddError.alreadyExists
[RevueSDK](../../index.md) › [FriendAddError](../index.md) › FriendAddError.alreadyExists
*Case*
對方已在好友列表中
```swift
case alreadyExists
```
---
# Error Implementations
[RevueSDK](../../index.md) › [FriendAddError](../index.md) › Error Implementations
*Article*
---
# errorDescription
[RevueSDK](../../index.md) › [FriendAddError](../index.md) › errorDescription
*Instance Property*
```swift
var errorDescription: String? { get }
```
---
# FriendAddError.failed(code:)
[RevueSDK](../../index.md) › [FriendAddError](../index.md) › FriendAddError.failed(code:)
*Case*
C 層回傳失敗
```swift
case failed(code: Int32)
```
---
# failureReason
[RevueSDK](../../index.md) › [FriendAddError](../index.md) › failureReason
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var failureReason: String? { get }
```
---
# helpAnchor
[RevueSDK](../../index.md) › [FriendAddError](../index.md) › helpAnchor
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var helpAnchor: String? { get }
```
---
# FriendAddError
[RevueSDK](../index.md) › FriendAddError
*Enumeration*
加好友錯誤
```swift
enum FriendAddError
```
## Topics
### Enumeration Cases
- [`case alreadyExists`](alreadyexists/index.md)
對方已在好友列表中
- [`case failed(code: Int32)`](failed%28code:%29/index.md)
C 層回傳失敗
### Instance Properties
- [`var errorDescription: String?`](errordescription/index.md)
---
# localizedDescription
[RevueSDK](../../index.md) › [FriendAddError](../index.md) › localizedDescription
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var localizedDescription: String { get }
```
---
# LocalizedError Implementations
[RevueSDK](../../index.md) › [FriendAddError](../index.md) › LocalizedError Implementations
*Article*
---
# recoverySuggestion
[RevueSDK](../../index.md) › [FriendAddError](../index.md) › recoverySuggestion
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var recoverySuggestion: String? { get }
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# FriendCallError.alreadyInCall
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › FriendCallError.alreadyInCall
*Case*
對方已有一通進行中(或尚未清除)的通話(C -28)
```swift
case alreadyInCall
```
## Discussion
若確信通話早該結束,可先呼叫掛斷再重試 —— 掛斷無連線前置條件, 即使該好友完全不可達仍可執行。
---
# FriendCallError.callFailed
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › FriendCallError.callFailed
*Case*
通話失敗(C -11)
```swift
case callFailed
```
## Discussion
C 層合併了「記憶體配置失敗 / codec 初始化失敗 / 其他 toxav 錯誤」,無法細分。
---
# Equatable Implementations
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › Equatable Implementations
*Article*
---
# Error Implementations
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › Error Implementations
*Article*
---
# errorDescription
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › errorDescription
*Instance Property*
```swift
var errorDescription: String? { get }
```
---
# failureReason
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › failureReason
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var failureReason: String? { get }
```
---
# FriendCallError.friendNotReachable
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › FriendCallError.friendNotReachable
*Case*
對方目前無法連線(C -20)—— 無任何可用 path
```swift
case friendNotReachable
```
---
# helpAnchor
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › helpAnchor
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var helpAnchor: String? { get }
```
---
# FriendCallError
[RevueSDK](../index.md) › FriendCallError
*Enumeration*
撥號 / 接聽 / 拒接 / 取消邀請的錯誤(`FriendManager` 撥號四方法 throw)
```swift
enum FriendCallError
```
## 錯誤碼對照(revuekit-c 0.17.0)
| C 常數 | 值 | case |
| --- | --- | --- |
| `REVUEKIT_ERR_INVALID_ARGUMENT` | -2 | [`FriendCallError.invalidArgument`](invalidargument/index.md) |
| `REVUEKIT_ERR_NOT_INITIALIZED` | -4 | [`FriendCallError.notInitialized`](notinitialized/index.md) |
| `REVUEKIT_ERR_CALL_FAILED` | -11 | [`FriendCallError.callFailed`](callfailed/index.md) |
| `REVUEKIT_ERR_INVALID_STATE` | -13 | [`FriendCallError.invalidState`](invalidstate/index.md) |
| `REVUEKIT_ERR_FRIEND_NOT_REACHABLE` | -20 | [`FriendCallError.friendNotReachable`](friendnotreachable/index.md) |
| `REVUEKIT_ERR_NO_DIRECT_PATH` | -26 | [`FriendCallError.noDirectPath`](nodirectpath/index.md) |
| `REVUEKIT_ERR_ALREADY_IN_CALL` | -28 | [`FriendCallError.alreadyInCall`](alreadyincall/index.md) |
| `REVUEKIT_ERR_MESSAGE_TOO_LONG` | -30 | [`FriendCallError.messageTooLong`](messagetoolong/index.md) |
| 表外 / `REVUEKIT_ERR_UNKNOWN`(-99) | * | [`FriendCallError.unknown(code:)`](unknown%28code:%29/index.md) |
## 為什麼不再需要「來源函式」
0.16 以前 `av_call` / `av_answer` / `av_hangup` 各自透傳三個不同的 toxav enum, 同一個負數在三個函式語意完全不同(例:`-4` 在 `av_call` 是「好友通話中」、 在 `av_answer` 是「codec 初始化失敗」),故當時需要 `Origin` 參數分派。
revuekit-c 0.17.0 (issue #86) 已將 AV 路徑改為全域唯一的 `REVUEKIT_ERR_*`, 並**保證同一情形在三個 API 回同一個碼**(由其單元測試 `test_same_code_means_same_thing_across_av_apis` 鎖住)。故 `Origin` 已移除, 本型別回歸單一對照表。
> **Note**
>
> 部分情形在 C 層被**刻意合併**、無法再細分: `-11` 涵蓋「記憶體配置失敗 / codec 初始化失敗 / 其他 toxav 錯誤」; `-2` 涵蓋「好友不存在 / friend_id 格式錯誤」。 上層若需更貼切的文案,可依自身呼叫情境(撥號 / 接聽 / 掛斷)自行判斷 —— 呼叫端本就知道自己在做哪件事,不需由本型別攜帶。
## Topics
### Enumeration Cases
- [`case alreadyInCall`](alreadyincall/index.md)
對方已有一通進行中(或尚未清除)的通話(C -28)
- [`case callFailed`](callfailed/index.md)
通話失敗(C -11)
- [`case friendNotReachable`](friendnotreachable/index.md)
對方目前無法連線(C -20)—— 無任何可用 path
- [`case invalidArgument`](invalidargument/index.md)
參數無效(C -2)—— 好友不存在,或 friend_id 格式錯誤(C 層已合併兩者)
- [`case invalidState`](invalidstate/index.md)
目前狀態不允許此操作(C -13)
- [`case messageTooLong`](messagetoolong/index.md)
撥號附帶訊息超過 256 bytes(C -30)
- [`case noDirectPath`](nodirectpath/index.md)
Switch B 寬限期內尚無直連路徑(C -26)
- [`case notInitialized`](notinitialized/index.md)
SDK / ToxAV 尚未初始化(C -4)
- [`case unknown(code: Int32)`](unknown%28code:%29/index.md)
未知錯誤,帶原始 C 碼供 debug(表外碼,含 `REVUEKIT_ERR_UNKNOWN` -99)
### Instance Properties
- [`var errorDescription: String?`](errordescription/index.md)
---
# FriendCallError.invalidArgument
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › FriendCallError.invalidArgument
*Case*
參數無效(C -2)—— 好友不存在,或 friend_id 格式錯誤(C 層已合併兩者)
```swift
case invalidArgument
```
---
# FriendCallError.invalidState
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › FriendCallError.invalidState
*Case*
目前狀態不允許此操作(C -13)
```swift
case invalidState
```
## Discussion
依呼叫情境有不同意義:`acceptInvitation` = 對方並未在呼叫本端(來電已結束); `hangUp` / `cancelInvitation` = 當前無通話(掛斷時對方已先結束,屬常見無害情形)。
---
# localizedDescription
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › localizedDescription
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var localizedDescription: String { get }
```
---
# LocalizedError Implementations
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › LocalizedError Implementations
*Article*
---
# FriendCallError.messageTooLong
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › FriendCallError.messageTooLong
*Case*
撥號附帶訊息超過 256 bytes(C -30)
```swift
case messageTooLong
```
## Discussion
⚠️ 亦可能由 Bridge 本地攔截產生(`RevueKitBridge.call` 在送進 C 之前先檢查、回 -30)。
---
# FriendCallError.noDirectPath
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › FriendCallError.noDirectPath
*Case*
Switch B 寬限期內尚無直連路徑(C -26)
```swift
case noDirectPath
```
## Discussion
這是**暫時狀態而非失敗** —— 直連建立或寬限期逾時後即可通話, 上層宜提示「正在建立直連,請稍候再試」並允許使用者重試。
---
# FriendCallError.notInitialized
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › FriendCallError.notInitialized
*Case*
SDK / ToxAV 尚未初始化(C -4)
```swift
case notInitialized
```
---
# recoverySuggestion
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › recoverySuggestion
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var recoverySuggestion: String? { get }
```
---
# FriendCallError.unknown(code:)
[RevueSDK](../../index.md) › [FriendCallError](../index.md) › FriendCallError.unknown(code:)
*Case*
未知錯誤,帶原始 C 碼供 debug(表外碼,含 `REVUEKIT_ERR_UNKNOWN` -99)
```swift
case unknown(code: Int32)
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# contains(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › contains(_:)
*Instance Method*
```swift
func contains(_ member: Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › Equatable Implementations
*Article*
---
# formIntersection(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › formIntersection(_:)
*Instance Method*
```swift
mutating func formIntersection(_ other: Self)
```
---
# formSymmetricDifference(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › formSymmetricDifference(_:)
*Instance Method*
```swift
mutating func formSymmetricDifference(_ other: Self)
```
---
# formUnion(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › formUnion(_:)
*Instance Method*
```swift
mutating func formUnion(_ other: Self)
```
---
# FriendConnectionPaths
[RevueSDK](../index.md) › FriendConnectionPaths
*Structure*
好友連線路徑(封裝 C 層 RevueKitFriendPathBitmap)
```swift
nonisolated struct FriendConnectionPaths
```
## Overview
對應 C 層 `revuekit_friend_get_paths()` 回傳的 bitmap。 每個 bit 表示一條獨立路徑是否可用,三條路徑可同時運作。 v0.13.0-rc1 移除「穩定/不穩定」維度,C 層只表達「路徑可用/不可用」。
## Topics
### Initializers
- [`init(rawValue: UInt8)`](init%28rawvalue:%29/index.md)
### Instance Properties
- [`var isOnline: Bool`](isonline/index.md)
是否在線(至少一條路徑可用)
- [`let rawValue: UInt8`](rawvalue/index.md)
### Type Properties
- [`static let tcpRelay: FriendConnectionPaths`](tcprelay/index.md)
TCP 中繼
- [`static let udpDirect: FriendConnectionPaths`](udpdirect/index.md)
UDP 直連(最佳路徑)
- [`static let udpRelay: FriendConnectionPaths`](udprelay/index.md)
UDP 中繼
---
# init()
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › init()
*Initializer*
```swift
init()
```
---
# init(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › init(_:)
*Initializer*
```swift
init(_ sequence: S) where S : Sequence, Self.Element == S.Element
```
---
# init(arrayLiteral:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › init(arrayLiteral:)
*Initializer*
```swift
init(arrayLiteral: Self.Element...)
```
---
# init(rawValue:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › init(rawValue:)
*Initializer*
```swift
init(rawValue: UInt8)
```
---
# insert(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › insert(_:)
*Instance Method*
```swift
@discardableResult mutating func insert(_ newMember: Self.Element) -> (inserted: Bool, memberAfterInsert: Self.Element)
```
---
# insert(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › insert(_:)
*Instance Method*
```swift
@discardableResult mutating func insert(_ newMember: Self.Element) -> (inserted: Bool, memberAfterInsert: Self.Element)
```
---
# intersection(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › intersection(_:)
*Instance Method*
```swift
func intersection(_ other: Self) -> Self
```
---
# isDisjoint(with:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › isDisjoint(with:)
*Instance Method*
```swift
func isDisjoint(with other: Self) -> Bool
```
---
# isEmpty
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › isEmpty
*Instance Property*
```swift
var isEmpty: Bool { get }
```
---
# isOnline
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › isOnline
*Instance Property*
是否在線(至少一條路徑可用)
```swift
var isOnline: Bool { get }
```
---
# isStrictSubset(of:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › isStrictSubset(of:)
*Instance Method*
```swift
func isStrictSubset(of other: Self) -> Bool
```
---
# isStrictSuperset(of:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › isStrictSuperset(of:)
*Instance Method*
```swift
func isStrictSuperset(of other: Self) -> Bool
```
---
# isSubset(of:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › isSubset(of:)
*Instance Method*
```swift
func isSubset(of other: Self) -> Bool
```
---
# isSuperset(of:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › isSuperset(of:)
*Instance Method*
```swift
func isSuperset(of other: Self) -> Bool
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# OptionSet Implementations
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › OptionSet Implementations
*Article*
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › RawRepresentable Implementations
*Article*
---
# rawValue
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › rawValue
*Instance Property*
```swift
let rawValue: UInt8
```
---
# remove(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › remove(_:)
*Instance Method*
```swift
@discardableResult mutating func remove(_ member: Self.Element) -> Self.Element?
```
---
# SetAlgebra Implementations
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › SetAlgebra Implementations
*Article*
---
# subtract(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › subtract(_:)
*Instance Method*
```swift
mutating func subtract(_ other: Self)
```
---
# subtracting(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › subtracting(_:)
*Instance Method*
```swift
func subtracting(_ other: Self) -> Self
```
---
# symmetricDifference(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › symmetricDifference(_:)
*Instance Method*
```swift
func symmetricDifference(_ other: Self) -> Self
```
---
# tcpRelay
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › tcpRelay
*Type Property*
TCP 中繼
```swift
static let tcpRelay: FriendConnectionPaths
```
---
# udpDirect
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › udpDirect
*Type Property*
UDP 直連(最佳路徑)
```swift
static let udpDirect: FriendConnectionPaths
```
---
# udpRelay
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › udpRelay
*Type Property*
UDP 中繼
```swift
static let udpRelay: FriendConnectionPaths
```
---
# union(_:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › union(_:)
*Instance Method*
```swift
func union(_ other: Self) -> Self
```
---
# update(with:)
[RevueSDK](../../index.md) › [FriendConnectionPaths](../index.md) › update(with:)
*Instance Method*
```swift
@discardableResult mutating func update(with newMember: Self.Element) -> Self.Element?
```
---
# Error Implementations
[RevueSDK](../../index.md) › [FriendDeleteError](../index.md) › Error Implementations
*Article*
---
# errorDescription
[RevueSDK](../../index.md) › [FriendDeleteError](../index.md) › errorDescription
*Instance Property*
```swift
var errorDescription: String? { get }
```
---
# FriendDeleteError.failed(code:)
[RevueSDK](../../index.md) › [FriendDeleteError](../index.md) › FriendDeleteError.failed(code:)
*Case*
C 層回傳失敗
```swift
case failed(code: Int32)
```
---
# failureReason
[RevueSDK](../../index.md) › [FriendDeleteError](../index.md) › failureReason
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var failureReason: String? { get }
```
---
# helpAnchor
[RevueSDK](../../index.md) › [FriendDeleteError](../index.md) › helpAnchor
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var helpAnchor: String? { get }
```
---
# FriendDeleteError
[RevueSDK](../index.md) › FriendDeleteError
*Enumeration*
刪除好友錯誤
```swift
enum FriendDeleteError
```
## Topics
### Enumeration Cases
- [`case failed(code: Int32)`](failed%28code:%29/index.md)
C 層回傳失敗
- [`case notFound`](notfound/index.md)
好友不存在
### Instance Properties
- [`var errorDescription: String?`](errordescription/index.md)
---
# localizedDescription
[RevueSDK](../../index.md) › [FriendDeleteError](../index.md) › localizedDescription
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var localizedDescription: String { get }
```
---
# LocalizedError Implementations
[RevueSDK](../../index.md) › [FriendDeleteError](../index.md) › LocalizedError Implementations
*Article*
---
# FriendDeleteError.notFound
[RevueSDK](../../index.md) › [FriendDeleteError](../index.md) › FriendDeleteError.notFound
*Case*
好友不存在
```swift
case notFound
```
---
# recoverySuggestion
[RevueSDK](../../index.md) › [FriendDeleteError](../index.md) › recoverySuggestion
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var recoverySuggestion: String? { get }
```
---
# acceptInvitation(friendId:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › acceptInvitation(friendId:)
*Instance Method*
接受對方邀請
```swift
@MainActor func acceptInvitation(friendId: String) throws
```
## Discussion
內部呼叫 `bridge.answer`,成功後注入 `.callState(isActive)` 事件 → Room 建立 RemoteParticipant 並 fire `participantDidConnect`。
為什麼要主動注入事件:ToxAV 對 caller 端會 fire callState=active 通知對方接聽成功; 但對 answerer 本端不會 fire(answer 成功 = 本機 state 同步翻譯成 active,C 層不再 產生事件)。因此 acceptInvitation 必須主動注入事件補上本端進房路徑,與 caller 端對稱。
注意:本 method 成功時**不直接** fire `didEndInvitation` — 接受邀請後狀態變 active 才算邀請結束(由注入的 callState 事件路由觸發)。
> **Throws**
>
> `FriendCallError`。常見:`.invalidState`(-13,對方並未在呼叫本端 —— 來電已結束或被其他裝置接走) / `.callFailed`(-11) / `.noDirectPath`(-26)。 **失敗時上層不應清掉來電 UI**,使用者可重試。
---
# addFriend(address:message:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › addFriend(address:message:)
*Instance Method*
加好友。對方已在好友列表時 throws .alreadyExists
```swift
func addFriend(address: String, message: String = "Hi") throws
```
## Discussion
C 層錯誤碼對照(toxcore `tox_friend_add` 錯誤碼取負值):
- `-5`:TOX_ERR_FRIEND_ADD_ALREADY_SENT(好友已存在)
- `-3`:TOX_ERR_FRIEND_ADD_NULL(message 為空,SDK 已自動補預設值)
⚠️ **升版時必須重新核對**(截至 revuekit-c 0.19.1 確認仍為下述狀態): 此處的 `-5` 是 **toxcore enum 透傳取負值**,**不是** `RevueKitError` 共用表 (共用表的 -5 是 `INIT_FAILED`,語意完全不同)。
revuekit-core `docs/contract/errors.md` 已規劃將 friend_add 併入共用表、 改以 `ALREADY_EXISTS(-21)` 表示「好友已存在」,但**尚未落地**: 0.19.1 的 C 實作仍回 -5,`revuekit.h` 的 enum 也還沒有 `ALREADY_EXISTS`。
該規劃一旦落地,此處的 `== -5` 會靜默失效(不編譯錯、不 crash, 只是 `.alreadyExists` 永不拋出、改拋 `.failed(code: -21)`)。 **文件不可信、以 C 原始碼為準**:升版時直接查 `revuekit-c/src/revuekit.c` 的 `revuekit_friend_add` 實作。
---
# addFriendNorequest(friendId:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › addFriendNorequest(friendId:)
*Instance Method*
接受好友請求(不發送請求)。對方已在好友列表時 throws .alreadyExists
```swift
func addFriendNorequest(friendId: String) throws
```
---
# cancelInvitation(friendId:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › cancelInvitation(friendId:)
*Instance Method*
撥打方取消尚未接通的邀請
```swift
@MainActor func cancelInvitation(friendId: String) throws
```
## Discussion
觸發 `RevueDelegate.didEndInvitation(reason: .cancelled)`。 若邀請已接通(對方已 accept),請改用 `Room.shared.remoteParticipants[id]?.disconnect()`。
> **Throws**
>
> `FriendCallError`(不受 Switch B 影響)。 常見:`.invalidState`(-13,當前無通話 —— 對方已先結束,屬無害情形) / `.invalidArgument`(-2)
---
# deleteFriend(friendId:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › deleteFriend(friendId:)
*Instance Method*
刪除好友。好友不存在時 throws .notFound
```swift
func deleteFriend(friendId: String) throws
```
---
# getFriendList()
[RevueSDK](../../index.md) › [FriendManager](../index.md) › getFriendList()
*Instance Method*
```swift
func getFriendList() -> [String]
```
---
# getFriendName(friendId:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › getFriendName(friendId:)
*Instance Method*
```swift
func getFriendName(friendId: String) -> String
```
---
# getFriendPaths(friendId:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › getFriendPaths(friendId:)
*Instance Method*
取得好友連線路徑(v0.13.0-rc1)
```swift
func getFriendPaths(friendId: String) -> FriendConnectionPaths
```
## Discussion
回傳 path bitmap(UDP 直連 / UDP 中繼 / TCP 中繼,可同時亮)。 `paths.isOnline` ⇔ 至少一條路徑可用。
---
# getFriendPresenceStatus(friendId:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › getFriendPresenceStatus(friendId:)
*Instance Method*
取得好友的 MQTT presence 狀態,與 tox 連線路徑(`getFriendPaths`)互相獨立。
```swift
func getFriendPresenceStatus(friendId: String) -> PresenceStatus
```
## Discussion
core 查不到時回 `.unknown`:presence 未配置、本機與 broker 未連線、好友沒回報過。 SDK 尚未初始化時回 `.offline`(對齊 Android `FriendManager.getFriendPresenceStatus`)。
---
# getFriendStatusMessage(friendId:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › getFriendStatusMessage(friendId:)
*Instance Method*
```swift
func getFriendStatusMessage(friendId: String) -> String
```
---
# FriendManager
[RevueSDK](../index.md) › FriendManager
*Class*
好友管理器
```swift
final class FriendManager
```
## Overview
封裝 RevueKit Core 的好友管理功能,提供好友新增、刪除、訊息收發等操作。
## Topics
### Instance Methods
- [`func acceptInvitation(friendId: String) throws`](acceptinvitation%28friendid:%29/index.md)
接受對方邀請
- [`func addFriend(address: String, message: String) throws`](addfriend%28address:message:%29/index.md)
加好友。對方已在好友列表時 throws .alreadyExists
- [`func addFriendNorequest(friendId: String) throws`](addfriendnorequest%28friendid:%29/index.md)
接受好友請求(不發送請求)。對方已在好友列表時 throws .alreadyExists
- [`func cancelInvitation(friendId: String) throws`](cancelinvitation%28friendid:%29/index.md)
撥打方取消尚未接通的邀請
- [`func deleteFriend(friendId: String) throws`](deletefriend%28friendid:%29/index.md)
刪除好友。好友不存在時 throws .notFound
- [`func getFriendList() -> [String]`](getfriendlist%28%29/index.md)
- [`func getFriendName(friendId: String) -> String`](getfriendname%28friendid:%29/index.md)
- [`func getFriendPaths(friendId: String) -> FriendConnectionPaths`](getfriendpaths%28friendid:%29/index.md)
取得好友連線路徑(v0.13.0-rc1)
- [`func getFriendPresenceStatus(friendId: String) -> PresenceStatus`](getfriendpresencestatus%28friendid:%29/index.md)
取得好友的 MQTT presence 狀態,與 tox 連線路徑(`getFriendPaths`)互相獨立。
- [`func getFriendStatusMessage(friendId: String) -> String`](getfriendstatusmessage%28friendid:%29/index.md)
- [`func isFriendExists(friendId: String) -> Bool`](isfriendexists%28friendid:%29/index.md)
檢查好友是否存在
- [`func rejectInvitation(friendId: String) throws`](rejectinvitation%28friendid:%29/index.md)
拒絕對方邀請
- [`func sendInvitation(friendId: String, message: String?) throws`](sendinvitation%28friendid:message:%29/index.md)
送出通話邀請給朋友
- [`func sendMessage(friendId: String, message: String, timeout: TimeInterval) async throws -> UInt32`](sendmessage%28friendid:message:timeout:%29/index.md)
發送文字訊息給好友並等待送達確認。
---
# isFriendExists(friendId:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › isFriendExists(friendId:)
*Instance Method*
檢查好友是否存在
```swift
func isFriendExists(friendId: String) -> Bool
```
---
# rejectInvitation(friendId:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › rejectInvitation(friendId:)
*Instance Method*
拒絕對方邀請
```swift
@MainActor func rejectInvitation(friendId: String) throws
```
## Discussion
觸發 `RevueDelegate.didEndInvitation(reason: .rejected)`。
> **Throws**
>
> `FriendCallError`。常見 `.invalidState`(-13,當前無通話 —— 對方已先取消)。 **失敗時上層仍應清掉來電 UI**(否則來電框關不掉)—— 與 `acceptInvitation` 相反。
---
# sendInvitation(friendId:message:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › sendInvitation(friendId:message:)
*Instance Method*
送出通話邀請給朋友
```swift
@MainActor func sendInvitation(friendId: String, message: String? = nil) throws
```
## Parameters
- friendId
對方好友 ID(Base58 字串)
- message
撥號時帶的 free-form opaque string(UTF-8 byte 上限 256,由 SDK 不檢查 / native 把關)
## Discussion
- 觸發 `RevueDelegate.didSendInvitation(friend:, message:)`(撥打方視角)
- 對方端會收到 `RevueDelegate.didReceiveInvitation(message:)`
- 對方接受邀請 → 雙邊 fire `RoomDelegate.participantDidConnect`, RemoteParticipant 才會建立並加入 `Room.shared.remoteParticipants`
- 對方拒絕 / 超時 / 網路中斷 → fire `RevueDelegate.didEndInvitation(reason:)`
**設計哲學**:撥號階段不協商媒體能力(audio/video bitrate 永遠預留 audio=48 / video=3000 kbps)。 「實際是否傳送音視訊」由通話建立後的 `LocalParticipant.publish*` / `sendVideoFrame` 控制。 `message` 為 free-form opaque string、SDK 不解析;上層(業務 App)可自由約定協定 (例:用 `"video"` / `"audio"` 標示來電類型,或塞 JSON envelope)。
> **Throws**
>
> `FriendCallError`(見該型別的對照表)。 常見:`.alreadyInCall`(-28) / `.friendNotReachable`(-20) / `.noDirectPath`(-26) / `.messageTooLong`(-30) / `.invalidArgument`(-2)
---
# sendMessage(friendId:message:timeout:)
[RevueSDK](../../index.md) › [FriendManager](../index.md) › sendMessage(friendId:message:timeout:)
*Instance Method*
發送文字訊息給好友並等待送達確認。
```swift
@discardableResult func sendMessage(friendId: String, message: String, timeout: TimeInterval = 10) async throws -> UInt32
```
## Parameters
- friendId
好友 Base58 ID
- message
訊息內容(UTF-8)
- timeout
等待 delivery receipt 的最長秒數。範圍 5~30 秒,預設 10 秒; 超出範圍不報錯、自動限制:大於 30 用 30,小於 5(含 0、負數)用 5; NaN 沒有大小可比,用預設的 10 秒
## Return Value
對方裝置已收到時回傳 toxcore 分配的 messageId
## Discussion
此方法會 await 直到兩種情況之一發生:
1. 對方裝置的 toxcore 收到封包並回 ACK → return messageId
2. 超過 `timeout` 秒數沒收到 ACK → throw `.timeout(messageId:)`
**v0.17.0 起送出前不以 paths 預判可達性**(契約 @invariant 3:bitmap 歸零期間 C 層 仍在重連):送出前不檢查 `isOnline`,一律直接交給 C 層、由回傳碼決定成敗。 送出**之後** paths 歸零則提前以 `.timeout` 收尾(見 `handleFriendPathsChange`)。
**並行呼叫支援**:多筆 `sendMessage` 可以並行呼叫,每筆獨立 await,互不影響。
**接收端不去重**:如果發送端 timeout 後重送,對方 toxcore 可能實際上已經收到第一筆, 導致對方 App 收到兩筆相同內容。去重邏輯由應用層在 message payload 中自行處理。
> **Throws**
>
> - `FriendMessageError.invalidArgument` — 好友不存在、ID 格式錯誤或訊息為空(C -1/-2)
> - `FriendMessageError.notInitialized` — SDK 尚未初始化(C -4)
> - `FriendMessageError.friendNotReachable` — 對方無可用 path(C -20,**可重試**)
> - `FriendMessageError.messageTooLong` — 訊息過長(C -30)
> - `FriendMessageError.sendQueueFull` — 發送佇列已滿(C -34,**可重試**)
> - `FriendMessageError.failed(code:)` — C 層回傳未對映的錯誤碼
> - `FriendMessageError.timeout(messageId:)` — 等待超過 timeout 秒數
> - `CancellationError` — `Revue.shared.destroy()` 觸發 cancelAllPending
>
> 好友存在性**不再由本方法預先檢查**,一律交給 C 層判定(回 `-2` → `.invalidArgument`):原前置檢查 `isFriendExists` 需解析整份好友清單, 是 async 暫停點、多則併發時造成 msgId 亂序配發。
---
# !=(_:_:)
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › Equatable Implementations
*Article*
---
# Error Implementations
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › Error Implementations
*Article*
---
# errorDescription
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › errorDescription
*Instance Property*
```swift
var errorDescription: String? { get }
```
---
# FriendMessageError.failed(code:)
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › FriendMessageError.failed(code:)
*Case*
C 層回傳未對映的錯誤碼(含 `REVUEKIT_ERR_UNKNOWN`(-99))
```swift
case failed(code: Int32)
```
---
# failureReason
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › failureReason
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var failureReason: String? { get }
```
---
# FriendMessageError.friendNotReachable
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › FriendMessageError.friendNotReachable
*Case*
對方目前無法連線(C -20)—— 無任何可用 path
```swift
case friendNotReachable
```
## Discussion
**可重試** —— 依 revuekit-c 0.17.0 契約 @invariant 3,path 歸零不代表 session 結束,核心層仍在重連。上層應等 `FRIEND_PATH_STATE_CHANGED` 顯示 path 恢復後再送。
---
# helpAnchor
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › helpAnchor
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var helpAnchor: String? { get }
```
---
# FriendMessageError
[RevueSDK](../index.md) › FriendMessageError
*Enumeration*
發送訊息錯誤
```swift
enum FriendMessageError
```
## 錯誤碼對照(revuekit-c 0.20.0)
| C 常數 | 值 | case |
| --- | --- | --- |
| `REVUEKIT_ERR_NULL_POINTER` | -1 | [`FriendMessageError.invalidArgument`](invalidargument/index.md) |
| `REVUEKIT_ERR_INVALID_ARGUMENT` | -2 | [`FriendMessageError.invalidArgument`](invalidargument/index.md) |
| `REVUEKIT_ERR_NOT_INITIALIZED` | -4 | [`FriendMessageError.notInitialized`](notinitialized/index.md) |
| `REVUEKIT_ERR_FRIEND_NOT_REACHABLE` | -20 | [`FriendMessageError.friendNotReachable`](friendnotreachable/index.md) |
| `REVUEKIT_ERR_MESSAGE_TOO_LONG` | -30 | [`FriendMessageError.messageTooLong`](messagetoolong/index.md) |
| `REVUEKIT_ERR_SEND_QUEUE_FULL` | -34 | [`FriendMessageError.sendQueueFull`](sendqueuefull/index.md) |
| 表外 / `REVUEKIT_ERR_UNKNOWN`(-99) | * | [`FriendMessageError.failed(code:)`](failed%28code:%29/index.md) |
revuekit-c 0.20.0 起 `revuekit_friend_send_message` 改用共用錯誤表 (`REVUEKIT_ERR_*`),同碼同義;此前為裸數字且與共用表撞號。
## Topics
### Enumeration Cases
- [`case failed(code: Int32)`](failed%28code:%29/index.md)
C 層回傳未對映的錯誤碼(含 `REVUEKIT_ERR_UNKNOWN`(-99))
- [`case friendNotReachable`](friendnotreachable/index.md)
對方目前無法連線(C -20)—— 無任何可用 path
- [`case invalidArgument`](invalidargument/index.md)
參數無效(C -1 / -2)—— 好友不存在、friend_id 格式錯誤,或訊息為空
- [`case messageTooLong`](messagetoolong/index.md)
訊息超過長度上限(C -30)
- [`case notInitialized`](notinitialized/index.md)
SDK 尚未初始化(C -4)
- [`case sendQueueFull`](sendqueuefull/index.md)
發送佇列已滿(C -34,revuekit-c 0.20.0 新增)
- [`case timeout(messageId: UInt32)`](timeout%28messageid:%29/index.md)
async 版本等待 delivery_receipt 超過 timeout 秒數 messageId 為 toxcore 分配的流水號,可用於日誌追蹤
### Instance Properties
- [`var errorDescription: String?`](errordescription/index.md)
---
# FriendMessageError.invalidArgument
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › FriendMessageError.invalidArgument
*Case*
參數無效(C -1 / -2)—— 好友不存在、friend_id 格式錯誤,或訊息為空
```swift
case invalidArgument
```
## Discussion
C 層已合併三種情形(`friend_id_to_friend_number` 回 UINT32_MAX 時 「格式錯」與「找不到」走同一條路),無法再細分。
---
# localizedDescription
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › localizedDescription
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var localizedDescription: String { get }
```
---
# LocalizedError Implementations
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › LocalizedError Implementations
*Article*
---
# FriendMessageError.messageTooLong
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › FriendMessageError.messageTooLong
*Case*
訊息超過長度上限(C -30)
```swift
case messageTooLong
```
---
# FriendMessageError.notInitialized
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › FriendMessageError.notInitialized
*Case*
SDK 尚未初始化(C -4)
```swift
case notInitialized
```
---
# recoverySuggestion
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › recoverySuggestion
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var recoverySuggestion: String? { get }
```
---
# FriendMessageError.sendQueueFull
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › FriendMessageError.sendQueueFull
*Case*
發送佇列已滿(C -34,revuekit-c 0.20.0 新增)
```swift
case sendQueueFull
```
## Discussion
**可重試** —— toxcore `TOX_ERR_FRIEND_SEND_MESSAGE_SENDQ`。 官方建議稍後重試或等 path 恢復後再送,**不應忙碌重試**。
---
# FriendMessageError.timeout(messageId:)
[RevueSDK](../../index.md) › [FriendMessageError](../index.md) › FriendMessageError.timeout(messageId:)
*Case*
async 版本等待 delivery_receipt 超過 timeout 秒數 messageId 為 toxcore 分配的流水號,可用於日誌追蹤
```swift
case timeout(messageId: UInt32)
```
---
# H264Encoding
[RevueSDK](../index.md) › H264Encoding
*Structure*
H.264 編碼參數
```swift
struct H264Encoding
```
## Overview
H.264 走外部硬體編碼(VideoToolbox)產生 Annex B、送給 `send_frame_encoded`。 目前 revuekit-c 對 H.264 沒有公開的深度旋鈕,僅開放 `maxBitrate`(會通知對端)。
## Topics
### Initializers
- [`init(maxBitrate: Int)`](init%28maxbitrate:%29/index.md)
### Instance Properties
- [`let maxBitrate: Int`](maxbitrate/index.md)
目標位元率(bps,預設 3 Mbps)
---
# init(maxBitrate:)
[RevueSDK](../../index.md) › [H264Encoding](../index.md) › init(maxBitrate:)
*Initializer*
```swift
init(maxBitrate: Int = 3_000_000)
```
---
# maxBitrate
[RevueSDK](../../index.md) › [H264Encoding](../index.md) › maxBitrate
*Instance Property*
目標位元率(bps,預設 3 Mbps)
```swift
let maxBitrate: Int
```
## Discussion
對應 C 層 `revuekit_av_video_set_bitrate`(內部除 1000 轉 kbps 送 C 層)。 下一幀生效。
---
# !=(_:_:)
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# bytesReceived
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › bytesReceived
*Instance Property*
與此對象累計收到位元組。連線顯示已連但 bytesReceived 不動 = 假連線。 來源:trafficStatsGetFriendRecord 累計值。即時速率 =(本次−上次)×8 / 取樣秒數 / 1000。
```swift
let bytesReceived: UInt64
```
---
# bytesSent
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › bytesSent
*Instance Property*
與此對象累計送出位元組。連線顯示已連但不動 = 假連線。 來源:trafficStatsGetFriendRecord 累計值。即時速率 =(本次−上次)×8 / 取樣秒數 / 1000。
```swift
let bytesSent: UInt64
```
---
# connectionType
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › connectionType
*Instance Property*
目前連線路徑類型:`0` = 無連線、`1` = TCP 中繼、`2` = UDP 中繼、`3` = UDP 直連。
```swift
let connectionType: Int32
```
## Discussion
v0.16.0 新增。**唯一不需前置條件的欄位** —— 只要好友存在即有值(不需通話中、 不需送過 ping),故可用來判斷「其他統計數值是否可信」: `connectionType == 0` 時所有數值都不應採信。 來源:revuekit-c `revuekit_friend_get_paths`(讀 friend path bitmap,無副作用)。
---
# downloadBitrateKbps
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › downloadBitrateKbps
*Instance Property*
下行可用碼率(kbps)。決定收到畫面清晰度;低 = 頻寬不足被降質。 來源:getNetworkQualityStats(C 提供)。
```swift
let downloadBitrateKbps: UInt32
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › Equatable Implementations
*Article*
---
# IceCandidatePairStatistics
[RevueSDK](../index.md) › IceCandidatePairStatistics
*Structure*
```swift
struct IceCandidatePairStatistics
```
## Topics
### Instance Properties
- [`let bytesReceived: UInt64`](bytesreceived/index.md)
與此對象累計收到位元組。連線顯示已連但 bytesReceived 不動 = 假連線。 來源:trafficStatsGetFriendRecord 累計值。即時速率 =(本次−上次)×8 / 取樣秒數 / 1000。
- [`let bytesSent: UInt64`](bytessent/index.md)
與此對象累計送出位元組。連線顯示已連但不動 = 假連線。 來源:trafficStatsGetFriendRecord 累計值。即時速率 =(本次−上次)×8 / 取樣秒數 / 1000。
- [`let connectionType: Int32`](connectiontype/index.md)
目前連線路徑類型:`0` = 無連線、`1` = TCP 中繼、`2` = UDP 中繼、`3` = UDP 直連。
- [`let downloadBitrateKbps: UInt32`](downloadbitratekbps/index.md)
下行可用碼率(kbps)。決定收到畫面清晰度;低 = 頻寬不足被降質。 來源:getNetworkQualityStats(C 提供)。
- [`var isReliable: Bool`](isreliable/index.md)
統計數據是否可信(v0.16.0 新增,衍生屬性、非 C 層提供)。
- [`let jitterMs: UInt32`](jitterms/index.md)
抖動(毫秒)。延遲的不穩定幅度;大 = 網路不穩、卡頓元兇之一。 來源:getNetworkQualityStats(C 提供)。
- [`let packetLossRate: Float`](packetlossrate/index.md)
丟包率(%)。最直接的連線劣化指標;高 = 破圖/斷續。 來源:getNetworkQualityStats(C 提供)。
- [`let packetsReceived: UInt64`](packetsreceived/index.md)
與此對象累計收到封包數。 來源:trafficStatsGetFriendRecord 累計值。
- [`let packetsSent: UInt64`](packetssent/index.md)
與此對象累計送出封包數。配合丟包率看:送出漲但對方收到漲慢 = 大量封包遺失。 來源:trafficStatsGetFriendRecord 累計值。
- [`let remoteSuggestedBitrateKbps: UInt32`](remotesuggestedbitratekbps/index.md)
對端建議碼率(kbps)。對方回報它能承受的碼率;持續偏低 = 對方網路差(非我方)。 來源:getNetworkQualityStats(C 提供)。
- [`let rttMs: Int32`](rttms/index.md)
往返延遲(毫秒)。反映連線延遲;<50 佳、100 普通、300+ 明顯卡頓。
- [`let uploadBitrateKbps: UInt32`](uploadbitratekbps/index.md)
上行可用碼率(kbps)。決定送出畫面清晰度;低 = 頻寬不足被降質。 來源:getNetworkQualityStats(C 提供)。
---
# isReliable
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › isReliable
*Instance Property*
統計數據是否可信(v0.16.0 新增,衍生屬性、非 C 層提供)。
```swift
var isReliable: Bool { get }
```
## 為什麼需要這個
C 層多個統計欄位在「前置條件未滿足」時一律回 **0 且回報成功**, 呼叫端無法分辨「真的是 0」與「沒有資料」:
- `packetLossRate` / `jitterMs` 需**通話中且有 video RTP session**(純語音恆 0)
- `uploadBitrateKbps` 為差分值,首次查詢與每次統計上傳後(約每分鐘)必為 0
結果是「三個無資料偽裝成完美數值」→ 評分公式算出滿分 → 顯示「極佳」, 即使一幀影像都沒收到。此屬性即為防止該誤導。
判定:有連線 **且** RTT 已量到。(「是否真的在收資料」需比對前後兩次 `bytesReceived` 是否成長,屬跨 tick 判斷,由 `Room` 統計層負責、不在單筆快照內。)
---
# jitterMs
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › jitterMs
*Instance Property*
抖動(毫秒)。延遲的不穩定幅度;大 = 網路不穩、卡頓元兇之一。 來源:getNetworkQualityStats(C 提供)。
```swift
let jitterMs: UInt32
```
---
# packetLossRate
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › packetLossRate
*Instance Property*
丟包率(%)。最直接的連線劣化指標;高 = 破圖/斷續。 來源:getNetworkQualityStats(C 提供)。
```swift
let packetLossRate: Float
```
---
# packetsReceived
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › packetsReceived
*Instance Property*
與此對象累計收到封包數。 來源:trafficStatsGetFriendRecord 累計值。
```swift
let packetsReceived: UInt64
```
---
# packetsSent
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › packetsSent
*Instance Property*
與此對象累計送出封包數。配合丟包率看:送出漲但對方收到漲慢 = 大量封包遺失。 來源:trafficStatsGetFriendRecord 累計值。
```swift
let packetsSent: UInt64
```
---
# remoteSuggestedBitrateKbps
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › remoteSuggestedBitrateKbps
*Instance Property*
對端建議碼率(kbps)。對方回報它能承受的碼率;持續偏低 = 對方網路差(非我方)。 來源:getNetworkQualityStats(C 提供)。
```swift
let remoteSuggestedBitrateKbps: UInt32
```
---
# rttMs
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › rttMs
*Instance Property*
往返延遲(毫秒)。反映連線延遲;<50 佳、100 普通、300+ 明顯卡頓。
```swift
let rttMs: Int32
```
## Discussion
**`-1` = 尚無資料**(v0.16.0 起;型別由 `UInt32` 改為 `Int32` 以容納此哨兵)。 RTT 是**雙邊協議**:雙方都必須至少呼過一次 `sendRttPing` 才會有值 —— C 層「收 ping 回 pong」的 callback 註冊在 `send_ping` 內部, 從未送過 ping 的裝置連對方的 ping 都不會回。 進房後約 3 秒(首次 ping 往返)內為 -1,屬正常暖機。
⚠️ UI 應顯示「—」而非「-1 ms」;`0` 則是真實量到的 0 毫秒。 來源:revuekit-c `revuekit_rtt_get`(純讀樣本,不重置任何計數器)。
---
# uploadBitrateKbps
[RevueSDK](../../index.md) › [IceCandidatePairStatistics](../index.md) › uploadBitrateKbps
*Instance Property*
上行可用碼率(kbps)。決定送出畫面清晰度;低 = 頻寬不足被降質。 來源:getNetworkQualityStats(C 提供)。
```swift
let uploadBitrateKbps: UInt32
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [InboundStatistics](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# audioFrameRate
[RevueSDK](../../index.md) › [InboundStatistics](../index.md) › audioFrameRate
*Instance Property*
每秒收到的音訊段數(音訊版「fps」)。穩定 = 音訊流順;掉低/歸零 = 音訊丟/斷。視訊軌道為 `nil`。 來源:SDK 收端數 audio callback 次數 / 秒(自算;音訊無「幀」概念,數「段」)。
```swift
let audioFrameRate: Double?
```
---
# audioLevel
[RevueSDK](../../index.md) › [InboundStatistics](../index.md) › audioLevel
*Instance Property*
收端音量。反映對方是否在出聲;與視訊配合分辨「音視訊都停=斷線 / 只視訊停=攝影機問題」。視訊軌道為 `nil`。 來源:SDK 收端 PCM 算 RMS(自算)。
```swift
let audioLevel: Float?
```
## Discussion
⚠️ 與 `Participant.audioLevel` **分工不同、不可互換**:
- 此處(診斷線):每段的**原始 RMS 瞬時值**,隨 `reportStatistics` 開關供診斷用
- `Participant.audioLevel`(產品線):語音聚焦輪詢算出的**平滑 + 量化**值,恆常運作
兩者共用同一次 `computeRMS`(RFC 6464 / W3C WebRTC-Stats 定義的本就是同一個 RMS), 聚焦讀 RTP header extension),非「同一個值放兩邊」。
---
# Equatable Implementations
[RevueSDK](../../index.md) › [InboundStatistics](../index.md) › Equatable Implementations
*Article*
---
# fps
[RevueSDK](../../index.md) › [InboundStatistics](../index.md) › fps
*Instance Property*
收端實測幀率。掉低 = 對方送不順/連線不穩。音訊軌道為 `nil`。 來源:SDK 收端數幀 / 秒(自算)。
```swift
let fps: Double?
```
---
# framesReceived
[RevueSDK](../../index.md) › [InboundStatistics](../index.md) › framesReceived
*Instance Property*
收到幀數(含 16×16 mute 哨兵)。反映「連線層有沒有在收東西」。 來源:VP8 在 handleVideoRawDataReceived 函式入口計數(哨兵判定在 L272、故入口計數天然含哨兵); ⚠️ H264 的哨兵判定在**解碼後 sink**(handleVideoEncodedDataReceived 內的解碼 closure), 故 H264 的 framesReceived 須在**解碼 closure 內**計數(放函式頂會漏算未解碼幀,且與哨兵含括語意不符)。
```swift
let framesReceived: UInt64
```
---
# framesRendered
[RevueSDK](../../index.md) › [InboundStatistics](../index.md) › framesRendered
*Instance Property*
真正送到繪圖的幀數(不含哨兵——哨兵已被 SDK mute 邏輯攔在繪圖前)。 診斷:framesReceived 續增但 framesRendered 停 = 對方 mute;兩者皆停 = 斷線。 來源:track.handleReceived(pixelBuffer:) 入口計數(RemoteParticipant.handleVideoFrameReceived)。統計不特判哨兵。
```swift
let framesRendered: UInt64
```
---
# freezeCount
[RevueSDK](../../index.md) › [InboundStatistics](../index.md) › freezeCount
*Instance Property*
凍結次數。畫面卡住不動的量化;抓「一頓一頓」比 fps 平均值更直接。 來源:SDK 自算——相鄰幀間隔 > 1 秒(凍結門檻)時 freezeCount++。
```swift
let freezeCount: UInt64
```
---
# height
[RevueSDK](../../index.md) › [InboundStatistics](../index.md) › height
*Instance Property*
```swift
let height: Int?
```
---
# InboundStatistics
[RevueSDK](../index.md) › InboundStatistics
*Structure*
收端統計(視訊 + 音訊)。
```swift
struct InboundStatistics
```
## Overview
同一個型別服務音訊與視訊軌道。對該種軌道**不適用**的欄位是 `nil`、不是 0 視訊軌道的 `audioFrameRate`、`audioLevel`。讀到 0 就是量到 0。
## Topics
### Instance Properties
- [`let audioFrameRate: Double?`](audioframerate/index.md)
每秒收到的音訊段數(音訊版「fps」)。穩定 = 音訊流順;掉低/歸零 = 音訊丟/斷。視訊軌道為 `nil`。 來源:SDK 收端數 audio callback 次數 / 秒(自算;音訊無「幀」概念,數「段」)。
- [`let audioLevel: Float?`](audiolevel/index.md)
收端音量。反映對方是否在出聲;與視訊配合分辨「音視訊都停=斷線 / 只視訊停=攝影機問題」。視訊軌道為 `nil`。 來源:SDK 收端 PCM 算 RMS(自算)。
- [`let fps: Double?`](fps/index.md)
收端實測幀率。掉低 = 對方送不順/連線不穩。音訊軌道為 `nil`。 來源:SDK 收端數幀 / 秒(自算)。
- [`let framesReceived: UInt64`](framesreceived/index.md)
收到幀數(含 16×16 mute 哨兵)。反映「連線層有沒有在收東西」。 來源:VP8 在 handleVideoRawDataReceived 函式入口計數(哨兵判定在 L272、故入口計數天然含哨兵); ⚠️ H264 的哨兵判定在**解碼後 sink**(handleVideoEncodedDataReceived 內的解碼 closure), 故 H264 的 framesReceived 須在**解碼 closure 內**計數(放函式頂會漏算未解碼幀,且與哨兵含括語意不符)。
- [`let framesRendered: UInt64`](framesrendered/index.md)
真正送到繪圖的幀數(不含哨兵——哨兵已被 SDK mute 邏輯攔在繪圖前)。 診斷:framesReceived 續增但 framesRendered 停 = 對方 mute;兩者皆停 = 斷線。 來源:track.handleReceived(pixelBuffer:) 入口計數(RemoteParticipant.handleVideoFrameReceived)。統計不特判哨兵。
- [`let freezeCount: UInt64`](freezecount/index.md)
凍結次數。畫面卡住不動的量化;抓「一頓一頓」比 fps 平均值更直接。 來源:SDK 自算——相鄰幀間隔 > 1 秒(凍結門檻)時 freezeCount++。
- [`let height: Int?`](height/index.md)
- [`let totalFreezesDuration: Double`](totalfreezesduration/index.md)
總凍結時長(秒)。 來源:SDK 自算——凍結門檻超出時累計間隔進 duration。
- [`let width: Int?`](width/index.md)
收端解出的畫面寬/高。突變 16×16 = 對方 mute 哨兵;歸零/跳變 = 解碼流中斷。 音訊軌道為 `nil`;視訊軌道在第一幀解出前也是 `nil`。 來源:複用 Track.dimensions(收端 RemoteVideoTrack 已在更新)。
---
# totalFreezesDuration
[RevueSDK](../../index.md) › [InboundStatistics](../index.md) › totalFreezesDuration
*Instance Property*
總凍結時長(秒)。 來源:SDK 自算——凍結門檻超出時累計間隔進 duration。
```swift
let totalFreezesDuration: Double
```
---
# width
[RevueSDK](../../index.md) › [InboundStatistics](../index.md) › width
*Instance Property*
收端解出的畫面寬/高。突變 16×16 = 對方 mute 哨兵;歸零/跳變 = 解碼流中斷。 音訊軌道為 `nil`;視訊軌道在第一幀解出前也是 `nil`。 來源:複用 Track.dimensions(收端 RemoteVideoTrack 已在更新)。
```swift
let width: Int?
```
---
# RevueSDK
RevueSDK
*Framework*
## Topics
### Classes
- [`class AudioBufferCapturer`](audiobuffercapturer/index.md)
Audio Buffer 擷取器 — 讓 App 注入任意 PCM 音訊
- [`class AudioCompositor`](audiocompositor/index.md)
音訊混音器 — 將多路 AudioTrack 混音為一路輸出(獨立工具)
- [`class AudioManager`](audiomanager/index.md)
音訊管理器 — 裝置管理、說話偵測、音量監控
- [`class AudioTrack`](audiotrack/index.md)
音訊 Track 中間類別 — 統一管理 AudioRenderer 分發
- [`class BroadcastManager`](broadcastmanager/index.md)
Manages the broadcast state and track publication for screen sharing on iOS.
- [`class BufferCapturer`](buffercapturer/index.md)
Buffer 擷取器 — 讓 App 注入任意 CVPixelBuffer 作為視訊來源
- [`class CameraCaptureOptions`](cameracaptureoptions/index.md)
相機擷取選項
- [`class CameraCapturer`](cameracapturer/index.md)
相機擷取器 — 控制相機硬體
- [`class FriendManager`](friendmanager/index.md)
好友管理器
- [`class LocalAudioTrack`](localaudiotrack/index.md)
本地音訊 Track — 麥克風或自訂音訊輸入
- [`class LocalParticipant`](localparticipant/index.md)
本地參與者(自己)— 發佈 Track、控制相機/麥克風/螢幕分享
- [`class LocalTrackPublication`](localtrackpublication/index.md)
本地 Track 發佈狀態
- [`class LocalVideoTrack`](localvideotrack/index.md)
本地視訊 Track — 相機、螢幕分享、或外部影像輸入
- [`class MicrophoneCapturer`](microphonecapturer/index.md)
麥克風擷取器 — 對外介面(薄殼,conform `AudioCapturer` protocol)
- [`class MulticastDelegate`](multicastdelegate/index.md)
多播代理容器 — 支援多個物件同時監聽事件
- [`class Participant`](participant/index.md)
參與者基礎類別 — LocalParticipant 和 RemoteParticipant 的共用屬性
- [`class PresenceManager`](presencemanager/index.md)
Presence 管理器
- [`class RemoteAudioTrack`](remoteaudiotrack/index.md)
遠端音訊 Track — 其他參與者的麥克風聲音
- [`class RemoteParticipant`](remoteparticipant/index.md)
遠端參與者(其他人)— 可訂閱其 Track、控制連線狀態
- [`class RemoteTrackPublication`](remotetrackpublication/index.md)
遠端 Track 發佈狀態
- [`class RemoteVideoTrack`](remotevideotrack/index.md)
遠端視訊 Track — 其他參與者的相機或螢幕分享畫面
- [`class Revue`](revue/index.md)
朋友層 singleton — SDK 入口點,管理好友關係、事件輪詢、delegate 推送
- [`class RevueSampleHandler`](revuesamplehandler/index.md)
Extension 端 base class — 開發者繼承後只需 override `enableLogging`(可選)
- [`class Room`](room/index.md)
會議室 — 管理參與者、Track 發佈/訂閱的核心物件(singleton)
- [`class ScreenShareCaptureOptions`](screensharecaptureoptions/index.md)
螢幕分享擷取選項
- [`class SecurityManager`](securitymanager/index.md)
安全管理器
- [`class Track`](track/index.md)
Track 基礎類別 — 一路獨立的媒體串流
- [`class TrackPublication`](trackpublication/index.md)
Track 發佈狀態 — 包裝 Track 的發佈/訂閱資訊
- [`class TrackRecorder`](trackrecorder/index.md)
Track 錄影器 — 將單一 VideoTrack + 單一 AudioTrack 錄製為 fMP4 檔案
- [`class VideoCapturer`](videocapturer/index.md)
視訊擷取器基底類別
- [`class VideoCompositor`](videocompositor/index.md)
視訊合成器 — 將多路 VideoTrack 按模板合成為一路輸出(獨立工具)
- [`class VideoTrack`](videotrack/index.md)
視訊 Track 中間類別 — 統一管理 VideoRenderer 分發
- [`class VideoView`](videoview/index.md)
視訊顯示元件 — UIView 容器,支援 Metal / SampleBuffer 雙模式渲染
### Protocols
- [`protocol AudioCapturer`](audiocapturer/index.md)
音訊擷取器基礎協定
- [`protocol AudioRenderer`](audiorenderer/index.md)
音訊渲染器協定 — 接收 Float32 PCM 音訊
- [`protocol BroadcastManagerDelegate`](broadcastmanagerdelegate/index.md)
A delegate protocol for receiving updates about the broadcast state.
- [`protocol ParticipantDelegate`](participantdelegate/index.md)
參與者事件代理 — 細粒度訂閱單一 Participant 的事件
- [`protocol RevueDelegate`](revuedelegate/index.md)
朋友層事件代理 — 監聽好友連線、訊息、Presence 等事件
- [`protocol RoomDelegate`](roomdelegate/index.md)
房間事件代理 — 監聽連線、通話、參與者、Track 等事件
- [`protocol TrackDelegate`](trackdelegate/index.md)
軌道事件代理 — 訂閱單一 `Track` 本身的事件
- [`protocol VideoCapturerDelegate`](videocapturerdelegate/index.md)
擷取器事件代理 — 訂閱解析度/狀態變更
- [`protocol VideoProcessor`](videoprocessor/index.md)
視訊處理器協定 — 可選的視訊處理管線
- [`protocol VideoRenderer`](videorenderer/index.md)
視訊渲染器協定 — VideoView 實作此協定接收畫面
- [`protocol VideoViewDelegate`](videoviewdelegate/index.md)
視訊顯示元件事件代理 — 訂閱單一 `VideoView` 的渲染狀態變化
### Structures
- [`struct AudioDevice`](audiodevice/index.md)
音訊裝置資訊
- [`struct AudioEncoding`](audioencoding/index.md)
音訊編碼設定
- [`struct AudioPublishOptions`](audiopublishoptions/index.md)
音訊發佈選項 — 設定音訊 Track 的編碼參數
- [`struct CodecStatistics`](codecstatistics/index.md)
當前視訊編碼統計。
- [`struct CompositeTemplate`](compositetemplate/index.md)
合成模板 — 定義多路 VideoTrack 的排版方式
- [`struct FriendConnectionPaths`](friendconnectionpaths/index.md)
好友連線路徑(封裝 C 層 RevueKitFriendPathBitmap)
- [`struct H264Encoding`](h264encoding/index.md)
H.264 編碼參數
- [`struct IceCandidatePairStatistics`](icecandidatepairstatistics/index.md)
- [`struct InboundStatistics`](inboundstatistics/index.md)
收端統計(視訊 + 音訊)。
- [`struct OutboundStatistics`](outboundstatistics/index.md)
發端送出統計。
- [`struct RecordingFileInfo`](recordingfileinfo/index.md)
錄影檔案資訊
- [`struct RevueLogEntry`](revuelogentry/index.md)
SDK log 條目(C 核心層 log 透過 callback 進來、Swift 端組成此型別推給訂閱者)
- [`struct TrackStatistics`](trackstatistics/index.md)
只保留「有料且對診斷有用」的桶(收斂記錄見 spec):
- [`struct VideoPublishOptions`](videopublishoptions/index.md)
視訊發佈選項 — 設定視訊 Track 的編碼參數
- [`struct VideoSourceStatistics`](videosourcestatistics/index.md)
發送側來源畫面統計(編碼前)。
- [`struct VideoTrackPositionInfo`](videotrackpositioninfo/index.md)
單一 Track 的位置資訊
- [`struct Vp8Encoding`](vp8encoding/index.md)
VP8 編碼參數
### Type Aliases
- [`typealias FrameDecryptCallback`](framedecryptcallback/index.md)
解密單幀 callback。回 0=成功;非 0=失敗(drop 該幀 + 發 `FrameCryptoError` 事件)。
- [`typealias FrameEncryptCallback`](frameencryptcallback/index.md)
加密單幀 callback。回 0=成功;非 0=失敗(drop 該幀 + 發 `FrameCryptoError` 事件)。
- [`typealias FrameGetMaxSizeCallback`](framegetmaxsizecallback/index.md)
查詢密文上限 callback。對相同 (mediaType, plaintextSize, userData state) 須 deterministic, 回傳值必須 ≥ encrypt 實際寫出的大小(XOR/AES-CTR 回 plaintextSize;AES-GCM 回 plaintextSize+16)。
### Enumerations
- [`enum AudioDuckingLevel`](audioduckinglevel/index.md)
- [`enum CameraCapturerError`](cameracapturererror/index.md)
相機擷取器的錯誤
- [`enum CaptureState`](capturestate/index.md)
擷取器狀態
- [`enum ConnectionQuality`](connectionquality/index.md)
參與者的連線品質(5 級)
- [`enum DegradationPreference`](degradationpreference/index.md)
品質降級偏好 — 頻寬不足時優先降什麼
- [`enum DisconnectReason`](disconnectreason/index.md)
通話中對方斷線原因
- [`enum DiskSpaceWarning`](diskspacewarning/index.md)
磁碟空間警告事件
- [`enum FrameCryptoError`](framecryptoerror/index.md)
Frame crypto 加解密逐幀錯誤(v0.14.0,C 層 `REVUEKIT_EVENT_FRAME_CRYPTO_ERROR` type 102)。
- [`enum FrameMediaType`](framemediatype/index.md)
Frame 媒體型別
- [`enum FriendAddError`](friendadderror/index.md)
加好友錯誤
- [`enum FriendCallError`](friendcallerror/index.md)
撥號 / 接聽 / 拒接 / 取消邀請的錯誤(`FriendManager` 撥號四方法 throw)
- [`enum FriendDeleteError`](frienddeleteerror/index.md)
刪除好友錯誤
- [`enum FriendMessageError`](friendmessageerror/index.md)
發送訊息錯誤
- [`enum InvitationEndReason`](invitationendreason/index.md)
邀請結束原因
- [`enum PresenceConnectionState`](presenceconnectionstate/index.md)
自身 Presence 服務(MQTT broker)連線狀態
- [`enum PresenceStatus`](presencestatus/index.md)
好友 Presence 狀態(對應 C 層 `RevueKitPresenceStatus`)
- [`enum RcMode`](rcmode/index.md)
VP8 Rate Control 模式
- [`enum RecorderState`](recorderstate/index.md)
錄影器狀態
- [`enum RecordingError`](recordingerror/index.md)
- [`enum RevueInitError`](revueiniterror/index.md)
SDK 初始化錯誤(`Revue.initialize` throw)
- [`enum RevueLogLevel`](revueloglevel/index.md)
log 級別(對應 C 層 `RevueKitLogLevel`,rawValue 直接對齊 Int32 ABI)
- [`enum RevuePresenceError`](revuepresenceerror/index.md)
Why `Revue.startPresence(_:)` failed.
- [`enum RoomError`](roomerror/index.md)
SDK 錯誤類型
- [`enum RoomMessageError`](roommessageerror/index.md)
Room 層訊息錯誤型別
- [`enum ScreenShareError`](screenshareerror/index.md)
螢幕分享錯誤型別
- [`enum SelfConnectionStatus`](selfconnectionstatus/index.md)
自身與 DHT 網路的連線狀態(對應 C 層 `RevueKitConnectionStatus`)
- [`enum UserStatus`](userstatus/index.md)
使用者狀態(對應 C 層 `RevueKitUserStatus`)
- [`enum VideoCodec`](videocodec/index.md)
視訊編碼器(含對應 encoding 參數)
- [`enum VideoCodecType`](videocodectype/index.md)
視訊編碼器類型(不帶 encoding 的 lightweight tag)
- [`enum VideoContentMode`](videocontentmode/index.md)
單一 Track 的內容縮放模式
- [`enum VideoRotation`](videorotation/index.md)
---
# !=(_:_:)
[RevueSDK](../../index.md) › [InvitationEndReason](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# InvitationEndReason.cancelled
[RevueSDK](../../index.md) › [InvitationEndReason](../index.md) › InvitationEndReason.cancelled
*Case*
撥打方取消邀請(撥打方收到此 reason 表示自己取消;接聽方收到表示對方取消)
```swift
case cancelled
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [InvitationEndReason](../index.md) › Equatable Implementations
*Article*
---
# InvitationEndReason.finished
[RevueSDK](../../index.md) › [InvitationEndReason](../index.md) › InvitationEndReason.finished
*Case*
對方結束邀請(C 層 `AV_CALL_STATE` FINISHED bit)
```swift
case finished
```
## Discussion
⚠️ 涵蓋「對方按拒接」與「對方按掛斷」—— 協定**刻意**不可分辨: 兩者送出的封包形狀相同。見 revuekit-core `docs/migrations/v0_16_to_v0_17.md`。
實務上邀請階段接聽方 UI 只有「接聽/拒接」兩鈕(掛斷需先接通, 而接通後改走 `DisconnectReason`),故此值在邀請階段實質等同「對方拒接」。
---
# InvitationEndReason
[RevueSDK](../index.md) › InvitationEndReason
*Enumeration*
邀請結束原因
```swift
enum InvitationEndReason
```
## Overview
用於 `RevueDelegate.didEndInvitation(reason:)`,雙邊視角共用。 App 從前面收到的 `didSendInvitation` 或 `didReceiveInvitation` 推斷自己角色。
## 來源與可信度
`cancelled` / `rejected` 由 SDK 在本端執行對應操作時直接 fire —— 語意確定。 `timeout` / `finished` 由 C 層 `AV_CALL_STATE` 的兩個 bit 轉譯而來, 該 bitmap 表達的是「**由誰判定通話結束**」而非「為什麼結束」, 故兩者各自涵蓋多種底層情境(見各 case 說明)。
## Topics
### Enumeration Cases
- [`case cancelled`](cancelled/index.md)
撥打方取消邀請(撥打方收到此 reason 表示自己取消;接聽方收到表示對方取消)
- [`case finished`](finished/index.md)
對方結束邀請(C 層 `AV_CALL_STATE` FINISHED bit)
- [`case rejected`](rejected/index.md)
接聽方拒絕邀請(撥打方收到表示對方拒絕;接聽方收到表示自己拒絕)
- [`case timeout`](timeout/index.md)
我方寬限期到期、放棄等待(C 層 `AV_CALL_STATE` ERROR bit)
---
# InvitationEndReason.rejected
[RevueSDK](../../index.md) › [InvitationEndReason](../index.md) › InvitationEndReason.rejected
*Case*
接聽方拒絕邀請(撥打方收到表示對方拒絕;接聽方收到表示自己拒絕)
```swift
case rejected
```
## Discussion
⚠️ 唯一產生點為本端 `rejectInvitation()` —— **撥打方不會收到此值**, 對方按拒接時本端收到的是 `.finished`(C 層送 FINISHED bit)。
---
# InvitationEndReason.timeout
[RevueSDK](../../index.md) › [InvitationEndReason](../index.md) › InvitationEndReason.timeout
*Case*
我方寬限期到期、放棄等待(C 層 `AV_CALL_STATE` ERROR bit)
```swift
case timeout
```
## Discussion
⚠️ 涵蓋「對方沒接」與「對方不可達(離線/關機)」兩種情境 —— C 層無法區分: MSI 協定無「對方響鈴中」的回覆訊號,撥號端收不到「對方已收到邀請」的確認。
---
# capturer
[RevueSDK](../../index.md) › [LocalAudioTrack](../index.md) › capturer
*Instance Property*
音訊擷取器
```swift
var capturer: AudioCapturer? { get }
```
---
# createBufferTrack(name:)
[RevueSDK](../../index.md) › [LocalAudioTrack](../index.md) › createBufferTrack(name:)
*Type Method*
建立自訂音訊輸入 Track(AudioBufferCapturer)
```swift
static func createBufferTrack(name: String = "audio_buffer") -> LocalAudioTrack
```
## Parameters
- name
Track 名稱
## Return Value
本地音訊 Track(可透過 capturer.capture(pcmBuffer) 注入音訊)
## Discussion
```swift
let audioTrack = LocalAudioTrack.createBufferTrack(name: "bgm")
let capturer = audioTrack.capturer as! AudioBufferCapturer
capturer.capture(pcmBuffer) // 注入任意音訊(音樂檔、TTS 等)
```
---
# LocalAudioTrack
[RevueSDK](../index.md) › LocalAudioTrack
*Class*
本地音訊 Track — 麥克風或自訂音訊輸入
```swift
class LocalAudioTrack
```
## Overview
```swift
// 麥克風(透過 setMicrophone 自動建立)
localParticipant.setMicrophone(enabled: true)
// 自訂音訊輸入(AudioBufferCapturer)
let audioTrack = LocalAudioTrack.createBufferTrack()
let capturer = audioTrack.capturer as! AudioBufferCapturer
capturer.capture(pcmBuffer)
```
## Topics
### Instance Properties
- [`var capturer: AudioCapturer?`](capturer/index.md)
音訊擷取器
### Instance Methods
- [`func mute() async throws`](mute%28%29/index.md)
作用依軌道而定:
- [`func unmute() async throws`](unmute%28%29/index.md)
取消靜音(對稱於 `mute()`:出口切回送真實聲音,麥克風軌道另解除 `AudioManager` 的靜音)。
### Type Methods
- [`static func createBufferTrack(name: String) -> LocalAudioTrack`](createbuffertrack%28name:%29/index.md)
建立自訂音訊輸入 Track(AudioBufferCapturer)
## Relationships
### Inherits From
- [`AudioTrack`](../audiotrack/index.md)
---
# mute()
[RevueSDK](../../index.md) › [LocalAudioTrack](../index.md) › mute()
*Instance Method*
作用依軌道而定:
```swift
@MainActor func mute() async throws
```
## Discussion
- 麥克風軌道:與 `LocalParticipant.set(source: .microphone, enabled: false)` 相同 —— 麥克風硬體 不停(`AudioManager.setMuted`,照常送全 0 才留得住 iOS 背景錄音豁免)。
- buffer 軌道(`createBufferTrack`):只把**這條軌道**送給所有接收者的內容換成全 0, **不碰 `AudioManager` 與麥克風** —— 靜音背景音樂時仍能說話。
只翻 `Track.isMuted` 並觸發一次 `TrackDelegate.track(_:didUpdateIsMuted:)`;**不改任何 publication 的 `isMuted`**,與 publication 層疊加(接收者實際收到 = 軌道 mute OR 他那份 mute)。 每位接收者的出口(`AudioConverter`)各存一份軌道層旗標,由 `LocalParticipant.applyAudioMute` 一起切; 之後才發布的出口在 `publish(audioTrack:)` 依 `isMuted` 初始化,一開始就送全 0。
2026-06-17 起不再發 ad envelope;mute 狀態由 raw data peakInt16 接收端判定。
---
# unmute()
[RevueSDK](../../index.md) › [LocalAudioTrack](../index.md) › unmute()
*Instance Method*
取消靜音(對稱於 `mute()`:出口切回送真實聲音,麥克風軌道另解除 `AudioManager` 的靜音)。
```swift
@MainActor func unmute() async throws
```
---
# audioPublication(to:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › audioPublication(to:)
*Instance Method*
查詢是否已對指定參與者發佈音訊 Track
```swift
func audioPublication(to friendId: String) -> LocalTrackPublication?
```
## Parameters
- friendId
對方的 friendId
## Return Value
已存在的 LocalTrackPublication,若尚未 publish 則回傳 nil
---
# cameraTrack
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › cameraTrack
*Instance Property*
相機 Track(lazy — 首次存取時建立,跨通話 / 房間持久,SDK destroy 才隨 LocalParticipant 陪葬)。
```swift
lazy var cameraTrack: LocalVideoTrack { get set }
```
## Discussion
- 建立不開硬體、不觸發相機權限(CameraCapturer init 僅配置閒置物件); 硬體由 `setCamera(enabled:)` 控制。
- `mutePlaceholder` 等 track 級設定可在任何時間點設置(不再受 「setCamera 是否呼叫過」約束)。
- ⚠️ main thread only(本 SDK 一律主執行緒操作之既有慣例)。
- ⚠️ lifecycle:`Revue.initialize()` 重建 LocalParticipant 時本屬性歸零重生, track 級設定(如 mutePlaceholder)需由上層重新設置。
---
# LocalParticipant
[RevueSDK](../index.md) › LocalParticipant
*Class*
本地參與者(自己)— 發佈 Track、控制相機/麥克風/螢幕分享
```swift
class LocalParticipant
```
## Overview
透過 `Room.shared.localParticipant` 取得,隨 SDK 初始化就存在。 撥號不在這裡:邀請好友用 `Revue.shared.friends.sendInvitation(friendId:message:)`, 對方接受後以 `RemoteParticipant` 出現在 `Room.shared.remoteParticipants`。
```swift
let local = Room.shared.localParticipant!
// 開硬體(不 publish)
local.setCamera(enabled: true)
local.setMicrophone(enabled: true)
// 發布給指定的人:每位接收者各發布一次,各得到一份 publication
let videoPub = try await local.publish(videoTrack: local.cameraTrack, to: participantA)
let audioPub = try await local.publish(audioTrack: local.microphoneTrack, to: participantA)
```
## Topics
### Instance Properties
- [`var cameraTrack: LocalVideoTrack`](cameratrack/index.md)
相機 Track(lazy — 首次存取時建立,跨通話 / 房間持久,SDK destroy 才隨 LocalParticipant 陪葬)。
- [`var isCameraActive: Bool`](iscameraactive/index.md)
相機的開關狀態(不代表已 publish、不代表 isMuted 狀態)
- [`var isMicrophoneActive: Bool`](ismicrophoneactive/index.md)
麥克風的開關狀態(不代表已 publish、不代表 isMuted 狀態)
- [`var isScreenSharing: Bool`](isscreensharing/index.md)
螢幕分享「是否進行中」狀態 — 統一 In-app / Broadcast 兩模式的對上層訊號。
- [`var isScreenSharingPublisher: AnyPublisher`](isscreensharingpublisher/index.md)
[isScreenSharing] 的變化流(CurrentValueSubject → 訂閱當下補發現值)。 對稱 `BroadcastManager.isBroadcastingPublisher`,但語意為「首幀為準」(見上)。
- [`var localAudioTracks: [LocalTrackPublication]`](localaudiotracks/index.md)
已發布的本地音訊 publication,規則同 `localVideoTracks`
- [`var localVideoTracks: [LocalTrackPublication]`](localvideotracks/index.md)
每位接收者各一份:同一條鏡頭軌道發布給 A、B,會得到 2 個 publication, 接收者分別是 A 與 B。螢幕分享也算視訊。未發布的軌道不在其中。
- [`var microphoneTrack: LocalAudioTrack`](microphonetrack/index.md)
麥克風 Track(lazy — 同 cameraTrack 語意;建立僅接上 MicrophoneCapturer.shared 對外殼 + 一條訂閱,不碰 AVAudioSession / AudioEngine、不觸發麥克風權限)。
- [`var screenShareTrack: LocalVideoTrack`](screensharetrack/index.md)
螢幕分享 Track(lazy — 同 cameraTrack 語意)
### Instance Methods
- [`func audioPublication(to: String) -> LocalTrackPublication?`](audiopublication%28to:%29/index.md)
查詢是否已對指定參與者發佈音訊 Track
- [`func publish(audioTrack: LocalAudioTrack, to: RemoteParticipant, options: AudioPublishOptions?) async throws -> LocalTrackPublication`](publish%28audiotrack:to:options:%29/index.md)
發佈音訊 Track 到指定參與者
- [`func publish(message: String, to: RemoteParticipant, timeout: TimeInterval) async throws -> UInt32`](publish%28message:to:timeout:%29/index.md)
發送訊息給指定參與者(Room 層訊息管道)
- [`func publish(videoTrack: LocalVideoTrack, to: RemoteParticipant, options: VideoPublishOptions?) async throws -> LocalTrackPublication`](publish%28videotrack:to:options:%29/index.md)
發佈視訊 Track 到指定參與者
- [`func publishCamera(to: RemoteParticipant, options: VideoPublishOptions?) async throws -> LocalTrackPublication`](publishcamera%28to:options:%29/index.md)
發佈相機給指定參與者(track 隨取隨有,上層不需要先呼叫 setCamera)
- [`func publishMicrophone(to: RemoteParticipant, options: AudioPublishOptions?) async throws -> LocalTrackPublication`](publishmicrophone%28to:options:%29/index.md)
發佈麥克風給指定參與者(track 隨取隨有,上層不需要先呼叫 setMicrophone)
- [`func set(name: String) -> Bool`](set%28name:%29/index.md)
設定自身名稱(Room 層 alias,底層走朋友層 toxcore broadcast)
- [`func set(source: Track.Source, enabled: Bool) async throws -> [LocalTrackPublication]`](set%28source:enabled:%29/index.md)
開關一條本地軌道(軌道層 mute):所有接收者一起受影響
- [`func set(status: UserStatus) -> Bool`](set%28status:%29/index.md)
設定自身使用者狀態(Room 層 alias)
- [`func set(statusMessage: String) -> Bool`](set%28statusmessage:%29/index.md)
設定自身狀態訊息(Room 層 alias)
- [`func setCamera(enabled: Bool, options: CameraCaptureOptions?) -> LocalVideoTrack?`](setcamera%28enabled:options:%29/index.md)
啟用/停用相機**硬體擷取**(不 publish、不改 `isMuted`)
- [`func setMicrophone(enabled: Bool) -> LocalAudioTrack?`](setmicrophone%28enabled:%29/index.md)
啟用/停用麥克風**硬體擷取**(不 publish、不改 `isMuted`)
- [`func setScreenShare(enabled: Bool) async throws -> LocalVideoTrack?`](setscreenshare%28enabled:%29/index.md)
啟用/停用螢幕分享**硬體擷取**(iOS)— 純設備控制,**不 publish**
- [`func unpublish(publication: LocalTrackPublication)`](unpublish%28publication:%29/index.md)
取消發佈 Track
- [`func unpublishAll()`](unpublishall%28%29/index.md)
取消發佈所有 Track
- [`func videoPublication(source: Track.Source, to: String) -> LocalTrackPublication?`](videopublication%28source:to:%29/index.md)
查詢是否已對指定參與者發佈視訊 Track
## Relationships
### Inherits From
- [`Participant`](../participant/index.md)
---
# isCameraActive
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › isCameraActive
*Instance Property*
相機的開關狀態(不代表已 publish、不代表 isMuted 狀態)
```swift
var isCameraActive: Bool { get }
```
## Discussion
- 與 `isCameraEnabled()` 的差異:
- `isCameraActive` — `setCamera(enabled:)` 開還是關
- 狀態在 `setCamera(enabled:)` 呼叫時**立即同步更新**,硬體 debounce 後才跟上;開不起來時改回 false。
---
# isMicrophoneActive
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › isMicrophoneActive
*Instance Property*
麥克風的開關狀態(不代表已 publish、不代表 isMuted 狀態)
```swift
var isMicrophoneActive: Bool { get }
```
## Discussion
- 與 `isMicrophoneEnabled()` 的差異:
- `isMicrophoneActive` — `setMicrophone(enabled:)` 開還是關
- 狀態在 `setMicrophone(enabled:)` 呼叫時**立即同步更新**。
---
# isScreenSharing
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › isScreenSharing
*Instance Property*
螢幕分享「是否進行中」狀態 — 統一 In-app / Broadcast 兩模式的對上層訊號。
```swift
var isScreenSharing: Bool { get }
```
## Discussion
本專案兩模式都要一等公民,故新增此統一訊號。**true/false 來源分模式**(使用者定案):
開 (true): - In-app → capturer 首幀送出(RPScreenRecorder 第一個 frame,見 observeScreenShareFirstFrame)。in-app 無 Extension 事件可用。 - Broadcast → 原生 `.broadcastStarted`(Extension 啟動即 true,見 handleBroadcastStateChanged)。用它自己的事件、語意乾淨。 關 (false): - In-app → `setScreenShare(false)` 呼叫當下(停是同步、確定的)。 - Broadcast → 原生 `.broadcastStopped`(等 Extension 真的停)。**UI 按鈕關** (requestStop)與**點狀態列紅色同心圓**兩條都走這條、收斂同一出口。
`BroadcastManager` 完全保留、不動 —— 它仍是 Broadcast 內部控制器(picker / requestStop / started/stopped 事件源)。此統一訊號只是把它與 in-app 首幀合流成 一個對上層的出口,讓上層(Plugin/Bloc)不必判模式。
---
# isScreenSharingPublisher
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › isScreenSharingPublisher
*Instance Property*
[isScreenSharing] 的變化流(CurrentValueSubject → 訂閱當下補發現值)。 對稱 `BroadcastManager.isBroadcastingPublisher`,但語意為「首幀為準」(見上)。
```swift
var isScreenSharingPublisher: AnyPublisher { get }
```
---
# localAudioTracks
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › localAudioTracks
*Instance Property*
已發布的本地音訊 publication,規則同 `localVideoTracks`
```swift
var localAudioTracks: [LocalTrackPublication] { get }
```
---
# localVideoTracks
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › localVideoTracks
*Instance Property*
每位接收者各一份:同一條鏡頭軌道發布給 A、B,會得到 2 個 publication, 接收者分別是 A 與 B。螢幕分享也算視訊。未發布的軌道不在其中。
```swift
var localVideoTracks: [LocalTrackPublication] { get }
```
---
# microphoneTrack
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › microphoneTrack
*Instance Property*
麥克風 Track(lazy — 同 cameraTrack 語意;建立僅接上 MicrophoneCapturer.shared 對外殼 + 一條訂閱,不碰 AVAudioSession / AudioEngine、不觸發麥克風權限)。
```swift
lazy var microphoneTrack: LocalAudioTrack { get set }
```
## Discussion
可用於:
- 加入 AudioCompositor 做混音(MCU 場景)
- 加入 TrackRecorder 做錄影
- 直接 publish 給其他人(P2P 場景)
```swift
let mixer = AudioCompositor()
mixer.addTrack(localParticipant.microphoneTrack)
```
---
# publish(audioTrack:to:options:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › publish(audioTrack:to:options:)
*Instance Method*
發佈音訊 Track 到指定參與者
```swift
func publish(audioTrack: LocalAudioTrack, to participant: RemoteParticipant, options: AudioPublishOptions? = nil) async throws -> LocalTrackPublication
```
## Discussion
建立 AudioConverter 發送 pipeline:MicrophoneCapturer output → fill(pcmBuffer) → Float32→Int16 + MTU 分包 → RevueKitBridge.sendAudioFrame。
**每位好友最多一路音訊**:核心層每個 friendId 只有一路 Opus 音訊(AAC 暫不使用)。 已對這位好友發布過音訊時,再次 publish 會 throw `RoomError.audioPublicationAlreadyExists`, 舊的發布保留不變;要換軌道,先 `unpublish(publication:)` 舊的再 publish 新的。
```swift
let mixForA = AudioCompositor()
mixForA.addTrack(localParticipant.microphoneTrack)
if let audioOfB = participantB.firstAudioTrack { mixForA.addTrack(audioOfB) }
try await localParticipant.publish(audioTrack: mixForA.outputTrack, to: participantA)
```
---
# publish(message:to:timeout:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › publish(message:to:timeout:)
*Instance Method*
發送訊息給指定參與者(Room 層訊息管道)
```swift
@discardableResult func publish(message: String, to participant: RemoteParticipant, timeout: TimeInterval = 10) async throws -> UInt32
```
## Parameters
- message
訊息文字
- participant
接收方 RemoteParticipant
- timeout
等待 delivery receipt 的最長秒數。範圍 5~30 秒,預設 10 秒; 超出範圍不報錯、自動限制:大於 30 用 30,小於 5(含 0、負數)用 5; NaN 沒有大小可比,用預設的 10 秒 (限制由 `FriendManager.sendMessage` 套用)
## Return Value
對方裝置 toxcore 回 ACK 時的 messageId
## Discussion
和 `FriendManager.sendMessage` 的差異:
- 朋友層 `sendMessage` — 送純字串給好友,觸發 `RevueDelegate.didReceiveMessage`
- Room 層 `publish(message:to:)` — 送 Room 訊息給 Room 內參與者, 觸發 `RoomDelegate.room(_:participant:didReceiveMessage:)`
兩者底層共用 toxcore transport,但在 wire format 和 delegate 路由上完全隔離。
> **Throws**
>
> - `RoomMessageError.encodingFailed` — JSON envelope 編碼失敗(罕見)
> - `RoomMessageError.messageTooLong` — envelope 超過 toxcore 1372 bytes 上限
> - `RoomMessageError.transportFailed(underlying:)` — 底層 FriendManager 失敗
> - `CancellationError` — Revue.destroy() 觸發
---
# publish(videoTrack:to:options:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › publish(videoTrack:to:options:)
*Instance Method*
發佈視訊 Track 到指定參與者
```swift
func publish(videoTrack: LocalVideoTrack, to participant: RemoteParticipant, options: VideoPublishOptions? = nil) async throws -> LocalTrackPublication
```
## Discussion
建立 VideoConverter 發送 pipeline:CameraCapturer output → fill(pixelBuffer) → Timer 取樣 → NV12→I420 → RevueKitBridge.sendVideoFrame。
**每位好友最多一路視訊**:已對這位好友發布過視訊(不論哪條軌道、哪種 codec)時, 再次 publish 會 throw `RoomError.videoPublicationAlreadyExists`,舊的發布保留不變。 要換軌道,先 `unpublish(publication:)` 舊的再 publish 新的;要換 codec,對既有 publication 呼叫 `LocalTrackPublication.setCodec(_:)`,不必重新發布。
發布不碰鏡頭硬體,和開鏡頭也沒有先後限制:軌道 mute 著或鏡頭關著時,對方收到佔位畫面。
同一個 Track 可以對不同人用不同 codec:
```swift
let track = localParticipant.cameraTrack
// 對 A 用 VP8(低延遲 preset)
try await localParticipant.publish(videoTrack: track, to: participantA,
options: VideoPublishOptions(codec: .vp8LowLatency))
// 對 B 用 H.264
try await localParticipant.publish(videoTrack: track, to: participantB,
options: VideoPublishOptions(codec: .h264Default))
```
---
# publishCamera(to:options:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › publishCamera(to:options:)
*Instance Method*
發佈相機給指定參與者(track 隨取隨有,上層不需要先呼叫 setCamera)
```swift
@discardableResult @MainActor func publishCamera(to participant: RemoteParticipant, options: VideoPublishOptions? = nil) async throws -> LocalTrackPublication
```
## Discussion
開硬體請另外呼叫 `setCamera(enabled: true)`。
---
# publishMicrophone(to:options:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › publishMicrophone(to:options:)
*Instance Method*
發佈麥克風給指定參與者(track 隨取隨有,上層不需要先呼叫 setMicrophone)
```swift
@discardableResult @MainActor func publishMicrophone(to participant: RemoteParticipant, options: AudioPublishOptions? = nil) async throws -> LocalTrackPublication
```
## Discussion
開硬體請另外呼叫 `setMicrophone(enabled: true)`。
---
# screenShareTrack
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › screenShareTrack
*Instance Property*
螢幕分享 Track(lazy — 同 cameraTrack 語意)
```swift
lazy var screenShareTrack: LocalVideoTrack { get set }
```
## Discussion
- 建立不啟動 Extension 進程、不彈系統對話框、不觸發 IPC socket 連線
- `mutePlaceholder` 等 track 級設定可在任何時間點設置
- Extension 進程由 `setScreenShare(enabled:)` 控制
- ⚠️ main thread only
- ⚠️ lifecycle:`Revue.initialize()` 重建 LocalParticipant 時本屬性歸零重生
---
# set(name:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › set(name:)
*Instance Method*
設定自身名稱(Room 層 alias,底層走朋友層 toxcore broadcast)
```swift
@discardableResult @MainActor func set(name: String) -> Bool
```
## Return Value
是否成功(C 層 ACK)
## Discussion
此方法等價於 `Revue.shared.setSelfName(_:)`。 影響範圍:**所有好友**(不只 Room 內)。
---
# set(source:enabled:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › set(source:enabled:)
*Instance Method*
開關一條本地軌道(軌道層 mute):所有接收者一起受影響
```swift
@discardableResult func set(source: Track.Source, enabled: Bool) async throws -> [LocalTrackPublication]
```
## Parameters
- source
`.camera` / `.microphone` / `.screenShareVideo`
- enabled
`.camera`:開、關硬體;其他:`true` = unmute 軌道、`false` = mute 軌道
## Return Value
這條軌道目前的 publication(每位接收者一份;沒有發布則為空)
## Discussion
只作用在軌道上(`Track.isMuted`),**不改任何 publication 的 `isMuted`**;兩層互不改寫, 接收者實際收到的是「軌道 mute,或他那份 publication mute」。要只對某一位停送, 改用那一份的 `LocalTrackPublication.mute()`。
| source | `enabled = false` | `enabled = true` |
| --- | --- | --- |
| `.camera` | 與 `setCamera(enabled: false)` 相同:鏡頭硬體停止、每位接收者收到佔位畫面,**不改 `isMuted`** | 與 `setCamera(enabled: true)` 相同 |
| `.microphone` | mute 軌道:每位接收者收到全 0 的聲音,硬體不停 | unmute 軌道 |
| `.screenShareVideo` | mute 軌道:每位接收者收到佔位畫面,**不撤下發布** | unmute 軌道 |
`.screenShareVideo` 關閉只 mute、保留 publication,與 `setScreenShare(enabled:)` 一致; 要讓對方的格子消失,明確呼叫 `unpublish(publication:)`。
不負責 publish:房內有沒有人、軌道發布給誰都不影響軌道狀態;之後發布時,軌道若仍是 mute 狀態,對方直接收到佔位畫面或靜音。軌道 `isMuted` 翻轉時觸發一次 `TrackDelegate.track(_:didUpdateIsMuted:)`;`participant(_:trackPublication:didUpdateIsMuted:)` 不會因此觸發。
`.camera` 只管硬體和出口,不觸發 `didUpdateIsMuted`;開不起來時只記 log、`isCameraActive` 改回 false, 不丟錯(和 `setCamera(enabled:)` 相同)。要阻斷鏡頭畫面但不動硬體,用 `cameraTrack.mute()`。
> **Throws**
>
> - `RoomMessageError.unsupportedSource` — 收到不支援的 source(例如 `.unknown`)
> - 其他底層軌道 mute/unmute 錯誤
---
# set(status:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › set(status:)
*Instance Method*
設定自身使用者狀態(Room 層 alias)
```swift
@discardableResult @MainActor func set(status: UserStatus) -> Bool
```
> **Deprecated**
>
> 目前對端不會收到事件(revuekit-c 0.11.x bug,issue #57)。請改用 PresenceManager 廣播狀態。
## Return Value
是否成功(C 層 ACK;目前永遠 true,但不代表對端會收到)
## Discussion
此方法等價於 `Revue.shared.setSelfStatus(_:)`。
⚠️ **目前對端不會收到事件**:revuekit-c 0.11.x 在接收端故意不註冊 `tox_callback_friend_status`,導致此 API 等於 no-op (呼叫成功 return true,但對端永遠收不到 `RevueDelegate.didUpdateStatus` / `RoomDelegate.didUpdateStatus`)。 追蹤中:https://git.ihh.tw/revue-kit/issue/-/issues/57 修復前若需要廣播 online/away/busy,請改走 MQTT Presence (`PresenceManager`,事件為 `friend_presence`)。
---
# set(statusMessage:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › set(statusMessage:)
*Instance Method*
設定自身狀態訊息(Room 層 alias)
```swift
@discardableResult @MainActor func set(statusMessage: String) -> Bool
```
## Return Value
是否成功(C 層 ACK)
## Discussion
此方法等價於 `Revue.shared.setSelfStatusMessage(_:)`。
---
# setCamera(enabled:options:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › setCamera(enabled:options:)
*Instance Method*
啟用/停用相機**硬體擷取**(不 publish、不改 `isMuted`)
```swift
@discardableResult @MainActor func setCamera(enabled: Bool, options: CameraCaptureOptions? = nil) -> LocalVideoTrack?
```
## Parameters
- enabled
true = 開硬體擷取、false = 關硬體擷取
- options
相機擷取選項(解析度、FPS、鏡頭位置)
## Return Value
相機 LocalVideoTrack(後續可透過 `publish(videoTrack:to:options:)` 發布給指定對象)
## Discussion
只開關鏡頭硬體,不發布、不翻任何 `isMuted` 旗標,也不觸發 mute 事件。關閉時若相機軌道 已發布,每位接收者的出口改送佔位畫面(`mutePlaceholder`,沒設則是 16×16 黑圖), 對方看到的是你關了鏡頭;重新開啟後恢復送真實畫面。軌道若已 mute (`cameraTrack.mute()`),重新開啟後仍送佔位畫面,直到 unmute。
與 `set(source: .camera, enabled:)` 相同。本方法不改 `isMuted`:關硬體不會歸零軌道 mute。 `cameraTrack` 跨房間常駐,用 `cameraTrack.mute()` 設的 mute 由 SDK 在離房 (`Room.disconnectAll()`)時歸零,進新房不必再清。
## 與 mute 的關係
mute 分兩層(軌道層、publication 層),和本方法是不同的 API:
| API | 作用範圍 | 硬體 | `isMuted` |
| --- | --- | --- | --- |
| `setCamera(enabled:)` / `set(source: .camera, enabled:)` | 所有接收者(關閉時送佔位畫面) | 開/關 | 不改 |
| `cameraTrack.mute()` / `unmute()` | 整條軌道,所有接收者 | 不碰 | 改 `Track.isMuted` |
| `LocalTrackPublication.mute()` / `unmute()` | 只有那一份 publication 的接收者 | 不碰 | 改那一份的 `isMuted` |
mute 狀態不另外送訊號:接收端從收到的畫面判斷(收到佔位畫面即視為 mute)。
```swift
// 只停硬體(例如本地不再需要鏡頭)
localParticipant.setCamera(enabled: false)
// 只對 A 關掉畫面:A 收到佔位畫面,其他人照常,硬體繼續跑
try await publicationToA.mute()
// 對所有人關掉畫面(鏡頭硬體停止,不改 isMuted)
try await localParticipant.set(source: .camera, enabled: false)
```
---
# setMicrophone(enabled:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › setMicrophone(enabled:)
*Instance Method*
啟用/停用麥克風**硬體擷取**(不 publish、不改 `isMuted`)
```swift
@discardableResult func setMicrophone(enabled: Bool) -> LocalAudioTrack?
```
## Parameters
- enabled
true = 開硬體擷取、false = 關硬體擷取
## Return Value
麥克風 LocalAudioTrack(後續可透過 `publish(audioTrack:to:options:)` 發布給指定對象)
## Discussion
只開關麥克風硬體(`AudioManager` 依此切換 AudioSession category),不發布、 不翻任何 `isMuted` 旗標,也不觸發 mute 事件。
## 與 mute 的關係
mute 分兩層(軌道層、publication 層),和本方法是不同的 API:
| API | 作用範圍 | 硬體 | `isMuted` |
| --- | --- | --- | --- |
| `setMicrophone(enabled:)` | 不針對接收者處理 | 開/關 | 不改 |
| `set(source: .microphone, enabled:)` / `microphoneTrack.mute()` | 整條軌道,所有接收者收到全 0 的聲音 | 不停 | 改 `Track.isMuted` |
| `LocalTrackPublication.mute()` / `unmute()` | 只有那一份 publication 的接收者收到全 0 的聲音 | 不碰 | 改那一份的 `isMuted` |
mute 狀態不另外送訊號:接收端從收到的聲音判斷(全 0 即視為 mute)。
內部走 `publication.mute/unmute` → `mediaTrack.isEnabled = false`(WebRTC 內部把 audio 取樣填零)。 AudioUnit / AudioDeviceModule 持續運轉,AudioSession 維持 `.playAndRecord`。
- **RevueSDK `setMicrophone(enabled:)`** 真的會關 mic 硬體擷取。 差異原因:RevueSDK 有「預覽錄影檔」等場景需要切回 `.playback` audio session category 取得正常喇叭音量; 只要 mic 硬體還在跑就必須維持 `.playAndRecord`,喇叭音量會被系統壓低(防回授)。 要「關麥克風但不停硬體」,用 `set(source: .microphone, enabled: false)`。
```swift
// 停硬體(例如要預覽錄影檔,AudioSession 可切回 .playback)
localParticipant.setMicrophone(enabled: false)
// 只對 A 靜音:A 收到全 0 的聲音,其他人照常
try await publicationToA.mute()
// 對所有人靜音(mute 軌道,硬體不停,可快速恢復)
try await localParticipant.set(source: .microphone, enabled: false)
```
---
# setScreenShare(enabled:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › setScreenShare(enabled:)
*Instance Method*
啟用/停用螢幕分享**硬體擷取**(iOS)— 純設備控制,**不 publish**
```swift
@discardableResult func setScreenShare(enabled: Bool) async throws -> LocalVideoTrack?
```
## Parameters
- enabled
true = 開硬體擷取, false = 關硬體擷取
## Return Value
螢幕分享 LocalVideoTrack(後續可透過 `publish(videoTrack:to:options:)` 發布給 指定對象)。**只要 enabled=true 被呼叫,track 就一定建好並回傳**(track / 硬體 / publish 三者獨立)—— 回傳非 nil 不代表「已在擷取」:Broadcast 第一階段回傳的 track 其 capturer 尚未 startCapture(等使用者按開始 → 第二階段自動啟動)。實際「擷取中」 狀態改由 `isScreenSharing` / `isScreenSharingPublisher` 判斷。型別保留 optional 僅 為 macOS 未實作等防禦路徑。
## Discussion
與 `setCamera` / `setMicrophone` 同一家族 —— 純硬體開關、回 track、 publish 分開。上層要送給誰,自行呼 `publish(videoTrack:to:)`。
原本 `setScreenShare(true)` 會偷偷對 Room 全員 publish,兩個問題:P2P 下追不上動態成員 (發完後才加入的人漏發),且會撞 publish 的單流名額 guard(該好友已有 video publish → throw)。
依 `ScreenShareCaptureOptions.defaultToBroadcastExtension` 自動偵測: 配了 Broadcast Extension → Broadcast 模式(可分享任何 App);否則 In-app 模式(只截 App 內)。
- `enabled = true`:
- Broadcast 模式:彈系統 picker,並立即回傳軌道(第一階段,capturer 尚未啟動); 使用者選 Extension 按開始後,由 broadcast observation 自動觸發第二階段啟動 capturer
- In-app 模式:等 `RPScreenRecorder` 啟動完成才回傳(可能 throw 權限錯誤)
- `enabled = false`:停 capturer;既有 screenShare publication 保留,出口改送佔位畫面 (不撤下發布;要撤下請呼叫 `unpublish(publication:)`)
> **Note**
>
> 保留 `async throws` —— ReplayKit(`RPScreenRecorder.startCapture`)啟動需 await 且可能拋權限錯,與 `setCamera` 的 AVCaptureSession 不同。
---
# unpublish(publication:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › unpublish(publication:)
*Instance Method*
取消發佈 Track
```swift
func unpublish(publication: LocalTrackPublication)
```
## Parameters
- publication
要取消的 LocalTrackPublication
## Discussion
停止向對方傳送此 Track 的資料,但 Track 物件本身不銷毀(可重新 publish)。
**維持同步** —— 停幀是本地即時動作(使用者按停,畫面該立刻停, 不能卡在等對方回應)。流程:①停幀 → ②移除 entry → ③送 undeclare(fire-and-forget) → ④fire didUnpublishTrack。
此處的 `VideoConverter.stop()` 是純軟體 cancel timer、不碰硬體。
**事件必發**:①②同步不 throw、③ fire-and-forget 不 throw ⇒ `didUnpublishTrack`
---
# unpublishAll()
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › unpublishAll()
*Instance Method*
取消發佈所有 Track
```swift
func unpublishAll()
```
## Discussion
停止所有發送 pipeline,清除所有 publication,並對每位接收者送和 `unpublish(publication:)` 相同的撤下通知(undeclare),對方畫面上的格子因此消失。通知盡力送達、失敗不回報, 本方法一定成功。Track 物件不銷毀。
收件人就是還有 entry 的對象:對方離開時 `Room.handleParticipantDisconnected` 已經用 `unpublishAll(to:)` 清掉他的 entry(不送,對方已不在),所以這裡只會通知還在房間裡的人。 `Room.disconnectAll()`(含 `destroy`)先逐一掛斷再呼叫本方法,此時已沒有 entry,自然不送。
---
# videoPublication(source:to:)
[RevueSDK](../../index.md) › [LocalParticipant](../index.md) › videoPublication(source:to:)
*Instance Method*
查詢是否已對指定參與者發佈視訊 Track
```swift
func videoPublication(source: Track.Source, to friendId: String) -> LocalTrackPublication?
```
## Parameters
- source
Track 來源(`.camera` 或 `.screenShareVideo`)
- friendId
對方的 friendId
## Return Value
已存在的 LocalTrackPublication,若尚未 publish 則回傳 nil
## Discussion
用於避免重複 publish 同一 source 給同一人。
---
# audioPublishOptions
[RevueSDK](../../index.md) › [LocalTrackPublication](../index.md) › audioPublishOptions
*Instance Property*
publish 時使用的音訊選項
```swift
var audioPublishOptions: AudioPublishOptions? { get }
```
---
# LocalTrackPublication
[RevueSDK](../index.md) › LocalTrackPublication
*Class*
本地 Track 發佈狀態
```swift
class LocalTrackPublication
```
## Overview
`localParticipant.setCamera(enabled: true)` 返回此物件。
## Topics
### Instance Properties
- [`var audioPublishOptions: AudioPublishOptions?`](audiopublishoptions/index.md)
publish 時使用的音訊選項
- [`var videoPublishOptions: VideoPublishOptions?`](videopublishoptions/index.md)
publish 時使用的視訊選項
### Instance Methods
- [`func mute() async throws`](mute%28%29/index.md)
只對這一份 publication 的接收者靜音(mute 兩層中的 publication 層)
- [`func requestKeyframe() throws`](requestkeyframe%28%29/index.md)
請求編碼器立即送 keyframe(畫面破圖恢復用)
- [`func setCodec(VideoCodec) throws`](setcodec%28_:%29/index.md)
切換視訊 codec(含 encoding)
- [`func setH264Encoding(H264Encoding) throws`](seth264encoding%28_:%29/index.md)
調整 H.264 encoding 參數(目前只公開 maxBitrate)
- [`func setMaxFps(Int) throws`](setmaxfps%28_:%29/index.md)
調整編碼器發送端 FPS 上限
- [`func setVp8Encoding(Vp8Encoding) throws`](setvp8encoding%28_:%29/index.md)
調整 VP8 encoding 參數(逐一呼叫 revuekit-c 的 VP8 setter)
- [`func unmute() async throws`](unmute%28%29/index.md)
取消這一份 publication 的靜音(見 `mute()`;軌道仍 mute 時對方照樣收到佔位畫面或靜音)
## Relationships
### Inherits From
- [`TrackPublication`](../trackpublication/index.md)
---
# mute()
[RevueSDK](../../index.md) › [LocalTrackPublication](../index.md) › mute()
*Instance Method*
只對這一份 publication 的接收者靜音(mute 兩層中的 publication 層)
```swift
func mute() async throws
```
## Discussion
只有這位接收者收到佔位畫面(視訊)或全 0 的聲音(音訊,照常送、不停送); 同一條軌道發給其他人的 publication 照常,**不碰軌道(`track.isMuted` 不變)與硬體**。 接收者實際收到的是「軌道 mute OR 這份 mute」:軌道已被 `set(source:enabled:)` 關閉時, 這裡 unmute 對方仍收到佔位畫面或靜音。
`isMuted` 只回報這一份;翻轉時只對這一份 fire `participant(_:trackPublication:didUpdateIsMuted:)`。
---
# requestKeyframe()
[RevueSDK](../../index.md) › [LocalTrackPublication](../index.md) › requestKeyframe()
*Instance Method*
請求編碼器立即送 keyframe(畫面破圖恢復用)
```swift
func requestKeyframe() throws
```
## Discussion
- VP8:呼叫 `revuekit_av_video_request_keyframe`
- H.264:呼叫 `VideoH264CodecTool.forceKeyframe`(下一幀 encode 時強制 IDR)
不動 `videoPublishOptions`(keyframe 是動作、非狀態)
> **Throws**
>
> `.trackNotPublished`
---
# setCodec(_:)
[RevueSDK](../../index.md) › [LocalTrackPublication](../index.md) › setCodec(_:)
*Instance Method*
切換視訊 codec(含 encoding)
```swift
func setCodec(_ codec: VideoCodec) throws
```
## Parameters
- codec
目標 codec + 對應 encoding 參數
## Discussion
立即切換 send pipeline:
- `.h264(...)` → 走 `revuekit_av_video_send_frame_encoded`(外部 VideoToolbox 編)
- `.vp8(...)` → 走 `revuekit_av_video_send_frame`(revuekit-c 內部軟體編)
若 codec type 沒變(例:VP8→VP8 只換 encoding),內部走 setVp8Encoding / setH264Encoding 邏輯、不重建 pipeline。
通話不中斷、不需要重談協商。切換 codec 時第一幀 keyframe 由 `VideoConverter.setActiveCodec` 內建 forceKeyframe 保證,對端解碼會有 <200ms 短暫卡頓後恢復。
> **Throws**
>
> - `.trackNotPublished` — publication 已 unpublish
> - `.invalidParameter` — encoding.maxBitrate <= 0
---
# setH264Encoding(_:)
[RevueSDK](../../index.md) › [LocalTrackPublication](../index.md) › setH264Encoding(_:)
*Instance Method*
調整 H.264 encoding 參數(目前只公開 maxBitrate)
```swift
func setH264Encoding(_ encoding: H264Encoding) throws
```
## Parameters
- encoding
H264 encoding 參數
## Discussion
呼叫 `VideoH264CodecTool.updateBitrate`(走 VideoToolbox `VTSessionSetProperty AverageBitRate`)。
> **Throws**
>
> - `.trackNotPublished`
> - `.codecNotSupported(codec: "h264")` — 當前 codec 非 h264
> - `.invalidParameter` — encoding.maxBitrate <= 0
---
# setMaxFps(_:)
[RevueSDK](../../index.md) › [LocalTrackPublication](../index.md) › setMaxFps(_:)
*Instance Method*
調整編碼器發送端 FPS 上限
```swift
func setMaxFps(_ fps: Int) throws
```
## Parameters
- fps
FPS 上限(1 ~ 60)
## Discussion
- VP8:改 VideoConverter timer 間隔
- H.264:改 VideoConverter timer 間隔(H.264 也經 VideoConverter 節流)
> **Throws**
>
> - `.trackNotPublished`
> - `.invalidParameter` — fps 不在 1…60
---
# setVp8Encoding(_:)
[RevueSDK](../../index.md) › [LocalTrackPublication](../index.md) › setVp8Encoding(_:)
*Instance Method*
調整 VP8 encoding 參數(逐一呼叫 revuekit-c 的 VP8 setter)
```swift
func setVp8Encoding(_ encoding: Vp8Encoding) throws
```
## Parameters
- encoding
完整 VP8 encoding 參數(可用 `.lowLatency` 等 preset)
## Discussion
詳見 `LocalParticipant.applyVp8Encoding` — 目前呼叫 6 個 C setter (bitrate / cpuUsed / quantizerRange / keyframeInterval / rcMode / temporalLayers)。 spec 提到的第 7 個「noise_sensitivity」Swift Bridge 尚未 forward、暫時跳過; Vp8Encoding 本身也沒此欄位,上層無從指定、無影響。
> **Throws**
>
> - `.trackNotPublished`
> - `.codecNotSupported(codec: "vp8")` — 當前 codec 非 vp8
> - `.invalidParameter` — encoding.maxBitrate <= 0
---
# unmute()
[RevueSDK](../../index.md) › [LocalTrackPublication](../index.md) › unmute()
*Instance Method*
取消這一份 publication 的靜音(見 `mute()`;軌道仍 mute 時對方照樣收到佔位畫面或靜音)
```swift
func unmute() async throws
```
---
# videoPublishOptions
[RevueSDK](../../index.md) › [LocalTrackPublication](../index.md) › videoPublishOptions
*Instance Property*
publish 時使用的視訊選項
```swift
@Published var videoPublishOptions: VideoPublishOptions? { get set }
```
## Discussion
Mid-call setter(`setCodec` / `setMaxFps` / `setVp8Encoding` / `setH264Encoding`) 執行成功後、SDK 內部會更新此值、讓上層讀到最新設定。
標 `@Published` 讓 Combine 訂閱者 / SwiftUI View 能自動收 setter 造成的變化。
---
# capturer
[RevueSDK](../../index.md) › [LocalVideoTrack](../index.md) › capturer
*Instance Property*
視訊擷取器(CameraCapturer 或 BufferCapturer)
```swift
var capturer: VideoCapturer? { get }
```
---
# createBufferTrack(name:source:)
[RevueSDK](../../index.md) › [LocalVideoTrack](../index.md) › createBufferTrack(name:source:)
*Type Method*
建立外部影像輸入 Track(BufferCapturer)
```swift
static func createBufferTrack(name: String = "buffer", source: Track.Source = .unknown) -> LocalVideoTrack
```
## Parameters
- name
Track 名稱(標籤,可重複)。`sid` 另行自動產生為 `"{name}-{短UUID}"`, ⚠️ `name` 會過線給對端(buffer track 的 declare 帶 name,對端以 source + name 認軌道、並當作遠端軌道的 `name`;`sid` 不過線),故「同名 track 同時 publish 給同一個人」對端分不出來 —— 該情境請給不同 name。 caller 根據用途宣告: - `.camera`:合成輸出 / 處理過的相機畫面(對端 UI 把它當主鏡頭看)
- `.screenShareVideo`:螢幕分享 / ReplayKit
- `.unknown`(預設):真的不確定來源(向後相容;對端 firstCameraVideoTrack / firstScreenShareVideoTrack 都找不到、UI 要自己撈)
## Return Value
本地視訊 Track(可透過 capturer.capture(pixelBuffer) 注入畫面)
## Discussion
```swift
// 一般 bufferTrack(沒指定用途)
let track = LocalVideoTrack.createBufferTrack(name: "compositor")
// MCU host 合成輸出(對 guest 而言是主鏡頭)
let track = LocalVideoTrack.createBufferTrack(name: "compositor", source: .camera)
// ReplayKit 螢幕分享
let track = LocalVideoTrack.createBufferTrack(name: "screen", source: .screenShareVideo)
// 同名可並存 —— 各自拿到獨立 sid
let a = LocalVideoTrack.createBufferTrack(name: "bgm") // sid = "bgm-A3F9C2D1"
let b = LocalVideoTrack.createBufferTrack(name: "bgm") // sid = "bgm-7B2E1D4F"
```
---
# LocalVideoTrack
[RevueSDK](../index.md) › LocalVideoTrack
*Class*
本地視訊 Track — 相機、螢幕分享、或外部影像輸入
```swift
class LocalVideoTrack
```
## Overview
建立方式:
```swift
// 相機:由 LocalParticipant 持有,硬體用 setCamera 開關
let camera = localParticipant.cameraTrack
localParticipant.setCamera(enabled: true, options: CameraCaptureOptions(fps: 30))
// 外部影像輸入(BufferCapturer):注入前要自己 startCapture()
let track = LocalVideoTrack.createBufferTrack()
let capturer = track.capturer as! BufferCapturer
try await capturer.startCapture()
capturer.capture(pixelBuffer)
```
## Topics
### Instance Properties
- [`var capturer: VideoCapturer?`](capturer/index.md)
視訊擷取器(CameraCapturer 或 BufferCapturer)
- [`var mutePlaceholder: CVPixelBuffer?`](muteplaceholder/index.md)
關閉相機時顯示的替代畫面(nil = 16×16 黑畫面)
- [`var processor: VideoProcessor?`](processor/index.md)
視訊處理器(例如背景模糊、浮水印,可選)
### Instance Methods
- [`func mute() async throws`](mute%28%29/index.md)
靜音整條軌道:每位接收者都收到佔位畫面,包括之後才發布的接收者。
- [`func unmute() async throws`](unmute%28%29/index.md)
取消靜音(對稱於 `mute()`):每位接收者的出口切回送真實畫面,不碰硬體;鏡頭關著時對方仍收到佔位畫面。 只翻 `Track.isMuted` 並觸發一次 `TrackDelegate.track(_:didUpdateIsMuted:)`;那一位的 publication 若自己 mute 著,他仍收到佔位畫面。
### Type Methods
- [`static func createBufferTrack(name: String, source: Track.Source) -> LocalVideoTrack`](createbuffertrack%28name:source:%29/index.md)
建立外部影像輸入 Track(BufferCapturer)
## Relationships
### Inherits From
- [`VideoTrack`](../videotrack/index.md)
---
# mute()
[RevueSDK](../../index.md) › [LocalVideoTrack](../index.md) › mute()
*Instance Method*
靜音整條軌道:每位接收者都收到佔位畫面,包括之後才發布的接收者。
```swift
@MainActor func mute() async throws
```
## Discussion
只管資料流動,**不碰硬體**:鏡頭開著就繼續擷取,`isCameraActive` 不變。要開關鏡頭硬體用 `LocalParticipant.setCamera(enabled:)` 或 `set(source: .camera, enabled:)`,這兩個方法也不會改 `isMuted`,所以軌道 mute 要由呼叫者自己解除。
只翻 `Track.isMuted` 並觸發一次 `TrackDelegate.track(_:didUpdateIsMuted:)`;**不改任何 publication 的 `isMuted`**,與 publication 層疊加(接收者實際收到 = 軌道 mute OR 他那份 mute)。之後才發布的出口在 `publish(videoTrack:)` 依 `isMuted` 初始化,一開始就送佔位畫面。
---
# mutePlaceholder
[RevueSDK](../../index.md) › [LocalVideoTrack](../index.md) › mutePlaceholder
*Instance Property*
關閉相機時顯示的替代畫面(nil = 16×16 黑畫面)
```swift
var mutePlaceholder: CVPixelBuffer? { get set }
```
## Discussion
`mutePlaceholder ?? 16×16 黑圖`(1fps)取代真實畫面。接收端收到 16×16 即視為 mute(fire didUpdateIsMuted + 攔下不繪),收到其他尺寸(客製圖 / 真實畫面)→ 直接畫。故客製圖請避開 16×16 解析度(一般圖片不會撞)。
格式:任何 CVPixelBuffer 格式皆可(BGRA / RGB / NV12…)— 設置時 SDK 自動 正規化為 NV12 + 偶數尺寸(VP8 發送管線 `nv12ToToxVideoFrame` 只吃 NV12; 未正規化的 BGRA 會被 silent 丟幀 → mute 期間對端凍結,2026-07-03 修)。
---
# processor
[RevueSDK](../../index.md) › [LocalVideoTrack](../index.md) › processor
*Instance Property*
視訊處理器(例如背景模糊、浮水印,可選)
```swift
var processor: VideoProcessor? { get set }
```
## Discussion
設了之後,每一幀在送出前先交給 `process(pixelBuffer:)`:回傳 nil 就丟掉這一幀; 本地預覽與送出都用處理後的幀。鏡頭、buffer、螢幕分享都經過,在擷取執行緒呼叫。 mute/背景時送的替代圖不經過 processor。
多執行緒同時存取(spec #86):App 在 main 寫、送幀流程在擷取執行緒每幀讀 → 鎖保護。
---
# unmute()
[RevueSDK](../../index.md) › [LocalVideoTrack](../index.md) › unmute()
*Instance Method*
取消靜音(對稱於 `mute()`):每位接收者的出口切回送真實畫面,不碰硬體;鏡頭關著時對方仍收到佔位畫面。 只翻 `Track.isMuted` 並觸發一次 `TrackDelegate.track(_:didUpdateIsMuted:)`;那一位的 publication 若自己 mute 著,他仍收到佔位畫面。
```swift
@MainActor func unmute() async throws
```
---
# captureState
[RevueSDK](../../index.md) › [MicrophoneCapturer](../index.md) › captureState
*Instance Property*
擷取器狀態(從內部 audioEngine isRunning 推導)
```swift
var captureState: CaptureState { get }
```
---
# MicrophoneCapturer
[RevueSDK](../index.md) › MicrophoneCapturer
*Class*
麥克風擷取器 — 對外介面(薄殼,conform `AudioCapturer` protocol)
```swift
class MicrophoneCapturer
```
## Overview
對齊 `CameraCapturer` / `InternalCameraCapturer` 兩層結構:對外殼住 `Capture/`、 內部硬體模組住 `Internal/Media/`。讓 protocol 演化(output / startCapture async 等)不污染受保護的內部模組。
透過 `LocalAudioTrack.capturer as? MicrophoneCapturer` 取得(麥克風 track)。
```swift
localParticipant.setMicrophone(enabled: true) // 內部會把對外殼塞進 track.capturer
```
**singleton 設計**:iOS `AVAudioEngine` input 全 process 唯一限制決定的—— 多個 LocalAudioTrack 共享同一條 PCM 來源。對應視訊側「相機可多支共存」的設計 差異,是物理硬體性質、非設計缺陷(見 MCU spec 設計討論)。
## Topics
### Instance Properties
- [`var captureState: CaptureState`](capturestate/index.md)
擷取器狀態(從內部 audioEngine isRunning 推導)
### Instance Methods
- [`func startCapture() async throws`](startcapture%28%29/index.md)
啟動擷取(包裝內部 sync `start()` 為 protocol 要求的 async)
- [`func stopCapture() async throws`](stopcapture%28%29/index.md)
停止擷取(包裝內部 sync `stop()` 為 protocol 要求的 async)
## Relationships
### Conforms To
- [`AudioCapturer`](../audiocapturer/index.md)
---
# startCapture()
[RevueSDK](../../index.md) › [MicrophoneCapturer](../index.md) › startCapture()
*Instance Method*
啟動擷取(包裝內部 sync `start()` 為 protocol 要求的 async)
```swift
func startCapture() async throws
```
---
# stopCapture()
[RevueSDK](../../index.md) › [MicrophoneCapturer](../index.md) › stopCapture()
*Instance Method*
停止擷取(包裝內部 sync `stop()` 為 protocol 要求的 async)
```swift
func stopCapture() async throws
```
---
# add(delegate:)
[RevueSDK](../../index.md) › [MulticastDelegate](../index.md) › add(delegate:)
*Instance Method*
新增代理
```swift
func add(delegate: T)
```
---
# MulticastDelegate
[RevueSDK](../index.md) › MulticastDelegate
*Class*
多播代理容器 — 支援多個物件同時監聽事件
```swift
class MulticastDelegate
```
## Topics
### Instance Methods
- [`func add(delegate: T)`](add%28delegate:%29/index.md)
新增代理
- [`func remove(delegate: T)`](remove%28delegate:%29/index.md)
移除代理
- [`func removeAll()`](removeall%28%29/index.md)
移除所有代理
---
# remove(delegate:)
[RevueSDK](../../index.md) › [MulticastDelegate](../index.md) › remove(delegate:)
*Instance Method*
移除代理
```swift
func remove(delegate: T)
```
---
# removeAll()
[RevueSDK](../../index.md) › [MulticastDelegate](../index.md) › removeAll()
*Instance Method*
移除所有代理
```swift
func removeAll()
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [OutboundStatistics](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# audioFrameRate
[RevueSDK](../../index.md) › [OutboundStatistics](../index.md) › audioFrameRate
*Instance Property*
每秒送出的音訊段數(發端音訊版「fps」)。與 Inbound 的 audioFrameRate 對稱。 來源:發送管線 sendAudioFrame 送出點數 / 秒(SDK 自算,總量語意 = 各對象各自送出的總和)。 視訊軌道為 `nil`(無音訊段);音訊軌道才有值。
```swift
let audioFrameRate: Double?
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [OutboundStatistics](../index.md) › Equatable Implementations
*Article*
---
# fps
[RevueSDK](../../index.md) › [OutboundStatistics](../index.md) › fps
*Instance Property*
送出實測幀率。音訊軌道為 `nil`。 來源:發送管線送出點數幀 / 秒(VP8 output + H264 h264EncodeOutput,SDK 自算)。 註:發端 bitrate 不做——VP8 實際送出量在 C 層 toxav 內壓縮(黑箱、Swift 拿不到), 無法與 H264 對稱;上行碼率改看 iceCandidatePair.uploadBitrateKbps。
```swift
let fps: Double?
```
---
# height
[RevueSDK](../../index.md) › [OutboundStatistics](../index.md) › height
*Instance Property*
```swift
let height: Int?
```
---
# OutboundStatistics
[RevueSDK](../index.md) › OutboundStatistics
*Structure*
發端送出統計。
```swift
struct OutboundStatistics
```
## Overview
同一個型別服務音訊與視訊軌道。對該種軌道**不適用**的欄位是 `nil`、不是 0 視訊軌道的 `audioFrameRate`。讀到 0 就是量到 0。
## Topics
### Instance Properties
- [`let audioFrameRate: Double?`](audioframerate/index.md)
每秒送出的音訊段數(發端音訊版「fps」)。與 Inbound 的 audioFrameRate 對稱。 來源:發送管線 sendAudioFrame 送出點數 / 秒(SDK 自算,總量語意 = 各對象各自送出的總和)。 視訊軌道為 `nil`(無音訊段);音訊軌道才有值。
- [`let fps: Double?`](fps/index.md)
送出實測幀率。音訊軌道為 `nil`。 來源:發送管線送出點數幀 / 秒(VP8 output + H264 h264EncodeOutput,SDK 自算)。 註:發端 bitrate 不做——VP8 實際送出量在 C 層 toxav 內壓縮(黑箱、Swift 拿不到), 無法與 H264 對稱;上行碼率改看 iceCandidatePair.uploadBitrateKbps。
- [`let height: Int?`](height/index.md)
- [`let width: Int?`](width/index.md)
送出畫面寬/高(編碼後實送規格)。與 videoSource 比對看降質來源。音訊軌道為 `nil`。 來源:發送管線送出點數(涵蓋 VP8 + H264 兩條,非只有 VP8 的 getVideoEncodingParams)。
---
# width
[RevueSDK](../../index.md) › [OutboundStatistics](../index.md) › width
*Instance Property*
送出畫面寬/高(編碼後實送規格)。與 videoSource 比對看降質來源。音訊軌道為 `nil`。 來源:發送管線送出點數(涵蓋 VP8 + H264 兩條,非只有 VP8 的 getVideoEncodingParams)。
```swift
let width: Int?
```
---
# audioLevel
[RevueSDK](../../index.md) › [Participant](../index.md) › audioLevel
*Instance Property*
即時音量(0.0 ~ 1.0)
```swift
@Published var audioLevel: Float { get set }
```
## Discussion
由 `Room` 的語音聚焦輪詢每 400ms 更新,存的是**量化後**的值(`ceil(level × 8) / 8`, 不在說話名單內者歸零。
---
# audioTracks
[RevueSDK](../../index.md) › [Participant](../index.md) › audioTracks
*Instance Property*
音訊 Track 列表(篩選 kind == .audio)
```swift
var audioTracks: [TrackPublication] { get }
```
---
# connectionQuality
[RevueSDK](../../index.md) › [Participant](../index.md) › connectionQuality
*Instance Property*
連線品質(5 級:excellent/good/fair/poor/bad + unknown)
```swift
@Published var connectionQuality: ConnectionQuality { get set }
```
## Discussion
由 `Room` 統計 timer 每 tick 經 `RemoteParticipant.connectionQualityEvaluator` 更新。 `ConnectionQualityEvaluator`。
---
# delegates
[RevueSDK](../../index.md) › [Participant](../index.md) › delegates
*Instance Property*
細粒度訂閱此 Participant 的事件
```swift
let delegates: MulticastDelegate
```
## Discussion
參與者事件兩層同步收到:同一事件會同時通知這裡(`ParticipantDelegate`)與 `Room.shared.delegates`(`RoomDelegate` 的對應方法)。例外:說話狀態在這裡是 `didUpdateIsSpeaking`,`RoomDelegate` 改收整份名單 `didUpdateSpeakingParticipants`。 會跳出提示的事件(收到靜音請求、收到訊息)只監聽其中一層,兩層都接會提示兩次。
---
# firstAudioTrack
[RevueSDK](../../index.md) › [Participant](../index.md) › firstAudioTrack
*Instance Property*
第一個音訊 Track(便捷存取)
```swift
var firstAudioTrack: AudioTrack? { get }
```
---
# firstCameraVideoTrack
[RevueSDK](../../index.md) › [Participant](../index.md) › firstCameraVideoTrack
*Instance Property*
第一個相機視訊 Track(便捷存取)
```swift
var firstCameraVideoTrack: VideoTrack? { get }
```
---
# firstScreenShareVideoTrack
[RevueSDK](../../index.md) › [Participant](../index.md) › firstScreenShareVideoTrack
*Instance Property*
第一個螢幕分享視訊 Track(便捷存取)
```swift
var firstScreenShareVideoTrack: VideoTrack? { get }
```
---
# id
[RevueSDK](../../index.md) › [Participant](../index.md) › id
*Instance Property*
```swift
var id: String { get }
```
---
# identity
[RevueSDK](../../index.md) › [Participant](../index.md) › identity
*Instance Property*
唯一識別碼(對應 friendId)
```swift
let identity: String
```
---
# Participant
[RevueSDK](../index.md) › Participant
*Class*
參與者基礎類別 — LocalParticipant 和 RemoteParticipant 的共用屬性
```swift
class Participant
```
## Overview
每個參與者帶著自己的 Track 進入房間,離開時帶走。
## Topics
### Instance Properties
- [`var audioLevel: Float`](audiolevel/index.md)
即時音量(0.0 ~ 1.0)
- [`var audioTracks: [TrackPublication]`](audiotracks/index.md)
音訊 Track 列表(篩選 kind == .audio)
- [`var connectionQuality: ConnectionQuality`](connectionquality/index.md)
連線品質(5 級:excellent/good/fair/poor/bad + unknown)
- [`let delegates: MulticastDelegate`](delegates/index.md)
細粒度訂閱此 Participant 的事件
- [`var firstAudioTrack: AudioTrack?`](firstaudiotrack/index.md)
第一個音訊 Track(便捷存取)
- [`var firstCameraVideoTrack: VideoTrack?`](firstcameravideotrack/index.md)
第一個相機視訊 Track(便捷存取)
- [`var firstScreenShareVideoTrack: VideoTrack?`](firstscreensharevideotrack/index.md)
第一個螢幕分享視訊 Track(便捷存取)
- [`var id: String`](id/index.md)
- [`let identity: String`](identity/index.md)
唯一識別碼(對應 friendId)
- [`var isSpeaking: Bool`](isspeaking/index.md)
是否正在說話
- [`var joinedAt: Date?`](joinedat/index.md)
加入時間
- [`var lastSpokeAt: Date?`](lastspokeat/index.md)
**起話時刻** —— 這次發言從何時開始(非「最後說話時間」)
- [`var name: String?`](name/index.md)
顯示名稱;沒有時為 nil(不是空字串)
- [`var statusMessage: String?`](statusmessage/index.md)
狀態訊息(個人簽名);沒有時為 nil(不是空字串)
- [`var trackPublications: [String : TrackPublication]`](trackpublications/index.md)
所有 Track 發佈 [Track.Sid: TrackPublication]
- [`var userStatus: UserStatus`](userstatus/index.md)
使用者狀態(none / away / busy)
- [`var videoTracks: [TrackPublication]`](videotracks/index.md)
視訊 Track 列表(篩選 kind == .video)
### Instance Methods
- [`func isCameraEnabled() -> Bool`](iscameraenabled%28%29/index.md)
`Participant.isCameraEnabled()` 同樣只看 `!isMuted`)。
- [`func isMicrophoneEnabled() -> Bool`](ismicrophoneenabled%28%29/index.md)
讀法與限制同 `isCameraEnabled()`;軌道層讀 `LocalParticipant.microphoneTrack.isMuted`。
- [`func isScreenShareEnabled() -> Bool`](isscreenshareenabled%28%29/index.md)
讀法與限制同 `isCameraEnabled()`;本地是否正在擷取看 `LocalParticipant.isScreenSharing`。
## Relationships
### Inherited By
- [`RemoteParticipant`](../remoteparticipant/index.md)
- [`LocalParticipant`](../localparticipant/index.md)
---
# isCameraEnabled()
[RevueSDK](../../index.md) › [Participant](../index.md) › isCameraEnabled()
*Instance Method*
`Participant.isCameraEnabled()` 同樣只看 `!isMuted`)。
```swift
func isCameraEnabled() -> Bool
```
## Discussion
mute 分兩層(軌道層、publication 層):在 `LocalParticipant` 上這裡讀的是 publication 層 (`LocalTrackPublication.mute()`),`cameraTrack.mute()` 與 `set(source: .camera, enabled: false)` 都不改 publication 的 `isMuted`,所以不會反映在這裡;軌道層讀 `cameraTrack.isMuted`, 鏡頭開關讀 `isCameraActive`。 本地發布給多人時每人一份 publication,這裡只看其中一份(字典順序,不固定是哪一位)。
---
# isMicrophoneEnabled()
[RevueSDK](../../index.md) › [Participant](../index.md) › isMicrophoneEnabled()
*Instance Method*
讀法與限制同 `isCameraEnabled()`;軌道層讀 `LocalParticipant.microphoneTrack.isMuted`。
```swift
func isMicrophoneEnabled() -> Bool
```
---
# isScreenShareEnabled()
[RevueSDK](../../index.md) › [Participant](../index.md) › isScreenShareEnabled()
*Instance Method*
讀法與限制同 `isCameraEnabled()`;本地是否正在擷取看 `LocalParticipant.isScreenSharing`。
```swift
func isScreenShareEnabled() -> Bool
```
---
# isSpeaking
[RevueSDK](../../index.md) › [Participant](../index.md) › isSpeaking
*Instance Property*
是否正在說話
```swift
@Published var isSpeaking: Bool { get set }
```
## Discussion
由 `Room` 的語音聚焦輪詢每 400ms 更新(見 `Room+ActiveSpeakers`)。判定不是單純 「音量過門檻」,而是「400ms 窗口內有聲時間佔比 ≥ 40%」—— 咳嗽 / 關門會被擋掉。 停止說話後 800ms 內熄滅。詳見 `SpeechActivityDetector`。
---
# joinedAt
[RevueSDK](../../index.md) › [Participant](../index.md) › joinedAt
*Instance Property*
加入時間
```swift
@Published var joinedAt: Date? { get set }
```
---
# lastSpokeAt
[RevueSDK](../../index.md) › [Participant](../index.md) › lastSpokeAt
*Instance Property*
**起話時刻** —— 這次發言從何時開始(非「最後說話時間」)
```swift
@Published var lastSpokeAt: Date? { get set }
```
## Discussion
「他剛剛還在講」這件事由 `isSpeaking` 表達;此屬性提供的是「發言起點」, 可用於排序(最近發言者優先)與顯示(「5 分鐘前發言」)。
---
# name
[RevueSDK](../../index.md) › [Participant](../index.md) › name
*Instance Property*
顯示名稱;沒有時為 nil(不是空字串)
```swift
@Published var name: String? { get set }
```
---
# objectWillChange
[RevueSDK](../../index.md) › [Participant](../index.md) › objectWillChange
*Instance Property*
- iOS 13.0+
- iPadOS 13.0+
- macCatalyst 13.0+
- macOS 10.15+
- tvOS 13.0+
- watchOS 6.0+
```swift
var objectWillChange: ObservableObjectPublisher { get }
```
---
# ObservableObject Implementations
[RevueSDK](../../index.md) › [Participant](../index.md) › ObservableObject Implementations
*Article*
---
# statusMessage
[RevueSDK](../../index.md) › [Participant](../index.md) › statusMessage
*Instance Property*
狀態訊息(個人簽名);沒有時為 nil(不是空字串)
```swift
@Published var statusMessage: String? { get set }
```
---
# trackPublications
[RevueSDK](../../index.md) › [Participant](../index.md) › trackPublications
*Instance Property*
所有 Track 發佈 [Track.Sid: TrackPublication]
```swift
@Published var trackPublications: [String : TrackPublication] { get set }
```
---
# userStatus
[RevueSDK](../../index.md) › [Participant](../index.md) › userStatus
*Instance Property*
使用者狀態(none / away / busy)
```swift
@Published var userStatus: UserStatus { get set }
```
---
# videoTracks
[RevueSDK](../../index.md) › [Participant](../index.md) › videoTracks
*Instance Property*
視訊 Track 列表(篩選 kind == .video)
```swift
var videoTracks: [TrackPublication] { get }
```
---
# ParticipantDelegate
[RevueSDK](../index.md) › ParticipantDelegate
*Protocol*
參與者事件代理 — 細粒度訂閱單一 Participant 的事件
```swift
protocol ParticipantDelegate : AnyObject
```
## Overview
`participant.delegates`(本協定)與 `Room.shared.delegates`(`RoomDelegate` 的對應方法)。 例外:說話狀態在本協定是 `didUpdateIsSpeaking`,`RoomDelegate` 改收整份名單 `didUpdateSpeakingParticipants`。
⚠️ 會跳出提示的事件(例如收到靜音請求就彈窗、收到訊息就推通知)**只監聽其中一層**, 兩層都接會提示兩次。
使用方式:
```swift
let alice = Room.shared.remoteParticipants["alice"]
alice?.delegates.add(delegate: self) // 只關心 alice 的變化
```
所有方法都有預設空實作,可選擇性實作需要的事件。
## Topics
### Instance Methods
- [`func participant(RemoteParticipant, didDisconnectWithReason: DisconnectReason)`](participant%28_:diddisconnectwithreason:%29/index.md)
- [`func participant(LocalParticipant, didPublishTrack: LocalTrackPublication)`](participant%28_:didpublishtrack:%29/index.md)
- [`func participant(RemoteParticipant, didReceiveMessage: String)`](participant%28_:didreceivemessage:%29/index.md)
- [`func participant(RemoteParticipant, didReceiveMuteRequest: Bool)`](participant%28_:didreceivemuterequest:%29/index.md)
- [`func participant(RemoteParticipant, didSubscribeTrack: RemoteTrackPublication)`](participant%28_:didsubscribetrack:%29/index.md)
- [`func participant(LocalParticipant, didUnpublishTrack: LocalTrackPublication)`](participant%28_:didunpublishtrack:%29/index.md)
- [`func participant(RemoteParticipant, didUnsubscribeTrack: RemoteTrackPublication)`](participant%28_:didunsubscribetrack:%29/index.md)
- [`func participant(Participant, didUpdateConnectionQuality: ConnectionQuality)`](participant%28_:didupdateconnectionquality:%29/index.md)
連線品質變更(5 級:excellent / good / fair / poor / bad)
- [`func participant(Participant, didUpdateIsSpeaking: Bool)`](participant%28_:didupdateisspeaking:%29/index.md)
說話狀態變更(**僅在 `isSpeaking` 真正翻轉時**觸發,不會每 tick 重複通知)
- [`func participant(Participant, didUpdateName: String?)`](participant%28_:didupdatename:%29/index.md)
參與者名稱已更新;對方清空名字時 `name` 為 nil
- [`func participant(Participant, didUpdatePaths: FriendConnectionPaths)`](participant%28_:didupdatepaths:%29/index.md)
參與者連線路徑變更(FriendConnectionPaths bitmap,v0.13.0-rc1)
- [`func participant(Participant, didUpdateStatus: UserStatus)`](participant%28_:didupdatestatus:%29/index.md)
參與者使用者狀態已更新(none/away/busy)
- [`func participant(Participant, didUpdateStatusMessage: String?)`](participant%28_:didupdatestatusmessage:%29/index.md)
參與者狀態訊息已更新;對方清空時 `message` 為 nil
- [`func participant(Participant, trackPublication: TrackPublication, didUpdateIsMuted: Bool)`](participant%28_:trackpublication:didupdateismuted:%29/index.md)
- [`func participant(Participant, trackPublication: TrackPublication, didUpdateStatistics: TrackStatistics)`](participant%28_:trackpublication:didupdatestatistics:%29/index.md)
Track 統計更新(每秒一次,僅 `Room.shared.reportStatistics == true` 時 fire、且有變化才 fire)。
---
# participant(_:didDisconnectWithReason:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didDisconnectWithReason:)
*Instance Method*
```swift
func participant(_ participant: RemoteParticipant, didDisconnectWithReason reason: DisconnectReason)
```
---
# participant(_:didDisconnectWithReason:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didDisconnectWithReason:)
*Instance Method*
```swift
func participant(_ participant: RemoteParticipant, didDisconnectWithReason reason: DisconnectReason)
```
---
# participant(_:didPublishTrack:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didPublishTrack:)
*Instance Method*
```swift
func participant(_ participant: LocalParticipant, didPublishTrack publication: LocalTrackPublication)
```
---
# participant(_:didPublishTrack:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didPublishTrack:)
*Instance Method*
```swift
func participant(_ participant: LocalParticipant, didPublishTrack publication: LocalTrackPublication)
```
---
# participant(_:didReceiveMessage:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didReceiveMessage:)
*Instance Method*
```swift
func participant(_ participant: RemoteParticipant, didReceiveMessage message: String)
```
---
# participant(_:didReceiveMessage:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didReceiveMessage:)
*Instance Method*
```swift
func participant(_ participant: RemoteParticipant, didReceiveMessage message: String)
```
---
# participant(_:didReceiveMuteRequest:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didReceiveMuteRequest:)
*Instance Method*
```swift
func participant(_ participant: RemoteParticipant, didReceiveMuteRequest isMuted: Bool)
```
---
# participant(_:didReceiveMuteRequest:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didReceiveMuteRequest:)
*Instance Method*
```swift
func participant(_ participant: RemoteParticipant, didReceiveMuteRequest isMuted: Bool)
```
---
# participant(_:didSubscribeTrack:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didSubscribeTrack:)
*Instance Method*
```swift
func participant(_ participant: RemoteParticipant, didSubscribeTrack publication: RemoteTrackPublication)
```
---
# participant(_:didSubscribeTrack:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didSubscribeTrack:)
*Instance Method*
```swift
func participant(_ participant: RemoteParticipant, didSubscribeTrack publication: RemoteTrackPublication)
```
---
# participant(_:didUnpublishTrack:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUnpublishTrack:)
*Instance Method*
```swift
func participant(_ participant: LocalParticipant, didUnpublishTrack publication: LocalTrackPublication)
```
---
# participant(_:didUnpublishTrack:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUnpublishTrack:)
*Instance Method*
```swift
func participant(_ participant: LocalParticipant, didUnpublishTrack publication: LocalTrackPublication)
```
---
# participant(_:didUnsubscribeTrack:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUnsubscribeTrack:)
*Instance Method*
```swift
func participant(_ participant: RemoteParticipant, didUnsubscribeTrack publication: RemoteTrackPublication)
```
---
# participant(_:didUnsubscribeTrack:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUnsubscribeTrack:)
*Instance Method*
```swift
func participant(_ participant: RemoteParticipant, didUnsubscribeTrack publication: RemoteTrackPublication)
```
---
# participant(_:didUpdateConnectionQuality:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUpdateConnectionQuality:)
*Instance Method*
連線品質變更(5 級:excellent / good / fair / poor / bad)
```swift
func participant(_ participant: Participant, didUpdateConnectionQuality quality: ConnectionQuality)
```
## Discussion
由 Room 統計 timer 每秒評估、**僅在等級改變時 fire**。 與 `didUpdatePaths`(路徑通不通)互補 —— 本 callback 表達「通著時好不好」。
---
# participant(_:didUpdateConnectionQuality:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUpdateConnectionQuality:)
*Instance Method*
```swift
func participant(_ participant: Participant, didUpdateConnectionQuality quality: ConnectionQuality)
```
---
# participant(_:didUpdateIsSpeaking:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUpdateIsSpeaking:)
*Instance Method*
說話狀態變更(**僅在 `isSpeaking` 真正翻轉時**觸發,不會每 tick 重複通知)
```swift
func participant(_ participant: Participant, didUpdateIsSpeaking isSpeaking: Bool)
```
## Discussion
由 Room 的語音聚焦輪詢每 400ms 判定;停止說話後 800ms 內收到 `false`。 與 `RoomDelegate.didUpdateSpeakingParticipants` 雙 fire。
---
# participant(_:didUpdateIsSpeaking:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUpdateIsSpeaking:)
*Instance Method*
```swift
func participant(_ participant: Participant, didUpdateIsSpeaking isSpeaking: Bool)
```
---
# participant(_:didUpdateName:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUpdateName:)
*Instance Method*
參與者名稱已更新;對方清空名字時 `name` 為 nil
```swift
func participant(_ participant: Participant, didUpdateName name: String?)
```
---
# participant(_:didUpdateName:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUpdateName:)
*Instance Method*
```swift
func participant(_ participant: Participant, didUpdateName name: String?)
```
---
# participant(_:didUpdatePaths:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUpdatePaths:)
*Instance Method*
參與者連線路徑變更(FriendConnectionPaths bitmap,v0.13.0-rc1)
```swift
func participant(_ participant: Participant, didUpdatePaths paths: FriendConnectionPaths)
```
## Discussion
paths 變空(全斷)時照常 fire,**不代表**通話結束、不會移除參與者:C 層仍在重連, 網路恢復後 paths 自動回報有連線。通話真正結束時才收到 `didDisconnectWithReason`。
---
# participant(_:didUpdatePaths:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUpdatePaths:)
*Instance Method*
```swift
func participant(_ participant: Participant, didUpdatePaths paths: FriendConnectionPaths)
```
---
# participant(_:didUpdateStatus:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUpdateStatus:)
*Instance Method*
參與者使用者狀態已更新(none/away/busy)
```swift
func participant(_ participant: Participant, didUpdateStatus status: UserStatus)
```
---
# participant(_:didUpdateStatus:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUpdateStatus:)
*Instance Method*
```swift
func participant(_ participant: Participant, didUpdateStatus status: UserStatus)
```
---
# participant(_:didUpdateStatusMessage:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUpdateStatusMessage:)
*Instance Method*
參與者狀態訊息已更新;對方清空時 `message` 為 nil
```swift
func participant(_ participant: Participant, didUpdateStatusMessage message: String?)
```
---
# participant(_:didUpdateStatusMessage:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:didUpdateStatusMessage:)
*Instance Method*
```swift
func participant(_ participant: Participant, didUpdateStatusMessage message: String?)
```
---
# participant(_:trackPublication:didUpdateIsMuted:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:trackPublication:didUpdateIsMuted:)
*Instance Method*
```swift
func participant(_ participant: Participant, trackPublication: TrackPublication, didUpdateIsMuted isMuted: Bool)
```
---
# participant(_:trackPublication:didUpdateIsMuted:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:trackPublication:didUpdateIsMuted:)
*Instance Method*
```swift
func participant(_ participant: Participant, trackPublication: TrackPublication, didUpdateIsMuted isMuted: Bool)
```
---
# participant(_:trackPublication:didUpdateStatistics:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:trackPublication:didUpdateStatistics:)
*Instance Method*
Track 統計更新(每秒一次,僅 `Room.shared.reportStatistics == true` 時 fire、且有變化才 fire)。
```swift
func participant(_ participant: Participant, trackPublication: TrackPublication, didUpdateStatistics statistics: TrackStatistics)
```
## Discussion
與 `@Published track.statistics` 同源:timer 每秒算完同時寫屬性 + fire 此事件。 SwiftUI 直接觀察 `track.statistics`、Flutter plugin 走此 delegate 轉 Dart stream。
---
# participant(_:trackPublication:didUpdateStatistics:)
[RevueSDK](../../index.md) › [ParticipantDelegate](../index.md) › participant(_:trackPublication:didUpdateStatistics:)
*Instance Method*
```swift
func participant(_ participant: Participant, trackPublication: TrackPublication, didUpdateStatistics statistics: TrackStatistics)
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# PresenceConnectionState.connected
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › PresenceConnectionState.connected
*Case*
已連線(broker 通訊正常)
```swift
case connected
```
---
# PresenceConnectionState.connecting
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › PresenceConnectionState.connecting
*Case*
連線中(首次連線)
```swift
case connecting
```
---
# PresenceConnectionState.disconnected
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › PresenceConnectionState.disconnected
*Case*
已斷線
```swift
case disconnected
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# PresenceConnectionState
[RevueSDK](../index.md) › PresenceConnectionState
*Enumeration*
自身 Presence 服務(MQTT broker)連線狀態
```swift
enum PresenceConnectionState
```
## Overview
描述 SDK 跟 MQTT broker 之間的連線狀態,獨立於 toxcore P2P / DHT。
⚠️ 與 `PresenceStatus` 區別:
- `PresenceConnectionState`:**自己**跟 broker 的連線(5 case,含未初始化、重連中)
- `PresenceStatus`:broker 看到的**對方**狀態(4 case,offline/online/away/busy)
當 `PresenceConnectionState` 不是 `connected` 時,所有透過 broker 取得的 `PresenceStatus` 資料皆不可信;`FriendConnectionPaths` bitmap 本身不受影響。
raw value 對應 C 層 `RevueKitPresenceConnectionState`。
## Topics
### Enumeration Cases
- [`case connected`](connected/index.md)
已連線(broker 通訊正常)
- [`case connecting`](connecting/index.md)
連線中(首次連線)
- [`case disconnected`](disconnected/index.md)
已斷線
- [`case notInitialized`](notinitialized/index.md)
未初始化(Presence 服務尚未啟動)
- [`case reconnecting`](reconnecting/index.md)
重新連線中(曾連上後斷線)
### Initializers
- [`init?(rawValue: Int32)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int32)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# PresenceConnectionState.notInitialized
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › PresenceConnectionState.notInitialized
*Case*
未初始化(Presence 服務尚未啟動)
```swift
case notInitialized
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › RawRepresentable Implementations
*Article*
---
# PresenceConnectionState.reconnecting
[RevueSDK](../../index.md) › [PresenceConnectionState](../index.md) › PresenceConnectionState.reconnecting
*Case*
重新連線中(曾連上後斷線)
```swift
case reconnecting
```
---
# getConnectionState()
[RevueSDK](../../index.md) › [PresenceManager](../index.md) › getConnectionState()
*Instance Method*
取得「自己跟 MQTT broker 的連線狀態」
```swift
func getConnectionState() -> PresenceConnectionState
```
## Discussion
5 態:
- `.notInitialized`:尚未初始化(Presence 服務還沒啟動)
- `.disconnected`:已斷線
- `.connecting`:首次連線中
- `.connected`:已連線(唯一 `getStatus / getFriendStatus` 回傳值可信的狀態)
- `.reconnecting`:曾連上後斷線、目前在重連
變化會透過 `RevueDelegate.didUpdatePresenceConnectionState` event 推送。
---
# getFriendStatus(friendId:)
[RevueSDK](../../index.md) › [PresenceManager](../index.md) › getFriendStatus(friendId:)
*Instance Method*
取得「某朋友的 Presence 狀態」(broker 端視角)
```swift
func getFriendStatus(friendId: String) -> PresenceStatus
```
## Discussion
broker 透過 MQTT 廣播得知對方狀態,獨立於 P2P paths:
- `.online`:broker 看到對方有裝置在線(**不代表 P2P 通了**)
- `.away / .busy`:對方主動設定
- `.offline`:broker 確實回報對方離線(含 LWT)
- `.unknown`:查不到——Presence 未配置、本機與 broker 未連線(`getConnectionState() != .connected`, 不回斷線前的快取)、好友未訂閱或尚未收到任何 broker 訊息(core 0.22.0)
---
# getStatus()
[RevueSDK](../../index.md) › [PresenceManager](../index.md) › getStatus()
*Instance Method*
取得「我自己目前廣播給 broker 的 Presence 狀態」
```swift
func getStatus() -> PresenceStatus
```
## Discussion
對應 `setStatus(_:)` 的讀值。SDK 啟動時 C 預設為 `.online`, 直到呼叫 `setStatus(_:)` 才改變。
---
# PresenceManager
[RevueSDK](../index.md) › PresenceManager
*Class*
Presence 管理器
```swift
final class PresenceManager
```
## Overview
MQTT Presence 服務(broker 廣播自身狀態 + 看朋友狀態)+ Signal 加速層 (自定 `revuekit/signal/` 信令交換 conn_info,加速 P2P 建立)。
**Presence vs P2P paths**:兩者**獨立**:
- paths(toxcore P2P)回答「我能不能跟對方通」
- Presence(MQTT broker)回答「對方有沒有開 App」
- broker 斷線時 `getFriendStatus` 回 `.unknown`(core 0.22.0 起不再回快取); `RevueDelegate.didUpdatePresenceConnectionState` 可得知斷線/重連
## Topics
### Instance Methods
- [`func getConnectionState() -> PresenceConnectionState`](getconnectionstate%28%29/index.md)
取得「自己跟 MQTT broker 的連線狀態」
- [`func getFriendStatus(friendId: String) -> PresenceStatus`](getfriendstatus%28friendid:%29/index.md)
取得「某朋友的 Presence 狀態」(broker 端視角)
- [`func getStatus() -> PresenceStatus`](getstatus%28%29/index.md)
取得「我自己目前廣播給 broker 的 Presence 狀態」
- [`func setStatus(PresenceStatus) -> Bool`](setstatus%28_:%29/index.md)
設定「我自己廣播給 broker 的 Presence 狀態」
---
# setStatus(_:)
[RevueSDK](../../index.md) › [PresenceManager](../index.md) › setStatus(_:)
*Instance Method*
設定「我自己廣播給 broker 的 Presence 狀態」
```swift
func setStatus(_ status: PresenceStatus) -> Bool
```
## Parameters
- status
要廣播的狀態(online / away / busy / offline)
## Return Value
`true` 設定成功(含 cache only 的情況),`false` 失敗
## Discussion
broker 收到後會廣播給所有訂閱我 presence topic 的朋友、 對方那邊觸發 `friend_presence` event 收到我的新狀態。
**broker 未連線時**:C 層仍會 cache 你設的值,broker 之後連上時 C 會自動補 publish 一次 — 不需要 Swift 端管。
---
# !=(_:_:)
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# PresenceStatus.away
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › PresenceStatus.away
*Case*
離開
```swift
case away
```
---
# PresenceStatus.busy
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › PresenceStatus.busy
*Case*
忙碌
```swift
case busy
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# PresenceStatus
[RevueSDK](../index.md) › PresenceStatus
*Enumeration*
好友 Presence 狀態(對應 C 層 `RevueKitPresenceStatus`)
```swift
enum PresenceStatus
```
## Overview
透過 MQTT 傳遞的網路 Presence,獨立於 toxcore P2P 連線狀態。 用於追蹤好友是否有裝置在線,即使 P2P 連線尚未建立。
⚠️ 與 `UserStatus` 區別:
- `PresenceStatus`:MQTT broker 看到的對方狀態(4 case,含 offline)
- `UserStatus`:toxcore P2P 廣播的使用者選擇狀態(3 case,無 offline)
raw value 對應 C 層 `revuekit_presence_get_friend_status()` / `revuekit_presence_set_status()` 數值。
## Topics
### Enumeration Cases
- [`case away`](away/index.md)
離開
- [`case busy`](busy/index.md)
忙碌
- [`case offline`](offline/index.md)
離線(broker 沒看到對方)
- [`case online`](online/index.md)
在線
- [`case unknown`](unknown/index.md)
Not answerable: presence not configured, broker not connected, friend never reported, or bad id (core 0.22.0).
### Initializers
- [`init?(rawValue: Int)`](init%28rawvalue:%29/index.md)
### Instance Properties
- [`var isOnline: Bool`](isonline/index.md)
True when the broker currently sees the friend (online / away / busy).
---
# init(rawValue:)
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int)
```
---
# isOnline
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › isOnline
*Instance Property*
True when the broker currently sees the friend (online / away / busy).
```swift
var isOnline: Bool { get }
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# PresenceStatus.offline
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › PresenceStatus.offline
*Case*
離線(broker 沒看到對方)
```swift
case offline
```
---
# PresenceStatus.online
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › PresenceStatus.online
*Case*
在線
```swift
case online
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › RawRepresentable Implementations
*Article*
---
# PresenceStatus.unknown
[RevueSDK](../../index.md) › [PresenceStatus](../index.md) › PresenceStatus.unknown
*Case*
Not answerable: presence not configured, broker not connected, friend never reported, or bad id (core 0.22.0).
```swift
case unknown
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [RcMode](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# RcMode.cbr
[RevueSDK](../../index.md) › [RcMode](../index.md) › RcMode.cbr
*Case*
Constant Bitrate — bitrate 固定、畫質浮動(適合直播 / 即時通訊)
```swift
case cbr
```
---
# RcMode.cq
[RevueSDK](../../index.md) › [RcMode](../index.md) › RcMode.cq
*Case*
Constrained Quality — 畫質為主、bitrate 為輔(弱網最佳)
```swift
case cq
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [RcMode](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../index.md) › [RcMode](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [RcMode](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# RcMode
[RevueSDK](../index.md) › RcMode
*Enumeration*
VP8 Rate Control 模式
```swift
enum RcMode
```
## Topics
### Enumeration Cases
- [`case cbr`](cbr/index.md)
Constant Bitrate — bitrate 固定、畫質浮動(適合直播 / 即時通訊)
- [`case cq`](cq/index.md)
Constrained Quality — 畫質為主、bitrate 為輔(弱網最佳)
- [`case vbr`](vbr/index.md)
Variable Bitrate — 畫質為目標、bitrate 浮動
### Initializers
- [`init?(rawValue: String)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../index.md) › [RcMode](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: String)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [RcMode](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [RcMode](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [RcMode](../index.md) › RawRepresentable Implementations
*Article*
---
# RcMode.vbr
[RevueSDK](../../index.md) › [RcMode](../index.md) › RcMode.vbr
*Case*
Variable Bitrate — 畫質為目標、bitrate 浮動
```swift
case vbr
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [RecorderState](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [RecorderState](../index.md) › Equatable Implementations
*Article*
---
# RecorderState.finalizing
[RevueSDK](../../index.md) › [RecorderState](../index.md) › RecorderState.finalizing
*Case*
收尾中(finishWriting 進行中,不可 start)
```swift
case finalizing
```
---
# hash(into:)
[RevueSDK](../../index.md) › [RecorderState](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [RecorderState](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# RecorderState.idle
[RevueSDK](../../index.md) › [RecorderState](../index.md) › RecorderState.idle
*Case*
未錄影
```swift
case idle
```
---
# RecorderState
[RevueSDK](../index.md) › RecorderState
*Enumeration*
錄影器狀態
```swift
enum RecorderState
```
## Topics
### Enumeration Cases
- [`case finalizing`](finalizing/index.md)
收尾中(finishWriting 進行中,不可 start)
- [`case idle`](idle/index.md)
未錄影
- [`case paused`](paused/index.md)
暫停中
- [`case recording`](recording/index.md)
錄影中
### Initializers
- [`init?(rawValue: Int)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../index.md) › [RecorderState](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [RecorderState](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# RecorderState.paused
[RevueSDK](../../index.md) › [RecorderState](../index.md) › RecorderState.paused
*Case*
暫停中
```swift
case paused
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [RecorderState](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [RecorderState](../index.md) › RawRepresentable Implementations
*Article*
---
# RecorderState.recording
[RevueSDK](../../index.md) › [RecorderState](../index.md) › RecorderState.recording
*Case*
錄影中
```swift
case recording
```
---
# Error Implementations
[RevueSDK](../../index.md) › [RecordingError](../index.md) › Error Implementations
*Article*
---
# errorDescription
[RevueSDK](../../index.md) › [RecordingError](../index.md) › errorDescription
*Instance Property*
```swift
var errorDescription: String? { get }
```
---
# failureReason
[RevueSDK](../../index.md) › [RecordingError](../index.md) › failureReason
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var failureReason: String? { get }
```
---
# helpAnchor
[RevueSDK](../../index.md) › [RecordingError](../index.md) › helpAnchor
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var helpAnchor: String? { get }
```
---
# RecordingError
[RevueSDK](../index.md) › RecordingError
*Enumeration*
```swift
enum RecordingError
```
## Topics
### Enumeration Cases
- [`case insufficientDiskSpace(available: Int64, required: Int64)`](insufficientdiskspace%28available:required:%29/index.md)
### Instance Properties
- [`var errorDescription: String?`](errordescription/index.md)
---
# RecordingError.insufficientDiskSpace(available:required:)
[RevueSDK](../../index.md) › [RecordingError](../index.md) › RecordingError.insufficientDiskSpace(available:required:)
*Case*
```swift
case insufficientDiskSpace(available: Int64, required: Int64)
```
---
# localizedDescription
[RevueSDK](../../index.md) › [RecordingError](../index.md) › localizedDescription
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var localizedDescription: String { get }
```
---
# LocalizedError Implementations
[RevueSDK](../../index.md) › [RecordingError](../index.md) › LocalizedError Implementations
*Article*
---
# recoverySuggestion
[RevueSDK](../../index.md) › [RecordingError](../index.md) › recoverySuggestion
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var recoverySuggestion: String? { get }
```
---
# createdAt
[RevueSDK](../../index.md) › [RecordingFileInfo](../index.md) › createdAt
*Instance Property*
建立時間
```swift
let createdAt: Date
```
---
# duration
[RevueSDK](../../index.md) › [RecordingFileInfo](../index.md) › duration
*Instance Property*
錄影時長(秒)
```swift
let duration: TimeInterval
```
---
# fileName
[RevueSDK](../../index.md) › [RecordingFileInfo](../index.md) › fileName
*Instance Property*
檔案名稱(含副檔名)
```swift
let fileName: String
```
---
# filePath
[RevueSDK](../../index.md) › [RecordingFileInfo](../index.md) › filePath
*Instance Property*
檔案完整路徑
```swift
let filePath: String
```
---
# fileSize
[RevueSDK](../../index.md) › [RecordingFileInfo](../index.md) › fileSize
*Instance Property*
檔案大小(bytes)
```swift
let fileSize: Int64
```
---
# fileURL
[RevueSDK](../../index.md) › [RecordingFileInfo](../index.md) › fileURL
*Instance Property*
檔案 URL
```swift
var fileURL: URL { get }
```
---
# id
[RevueSDK](../../index.md) › [RecordingFileInfo](../index.md) › id
*Instance Property*
檔案 ID(UUID)
```swift
let id: String
```
---
# RecordingFileInfo
[RevueSDK](../index.md) › RecordingFileInfo
*Structure*
錄影檔案資訊
```swift
struct RecordingFileInfo
```
## Topics
### Initializers
- [`init(id: String, fileName: String, filePath: String, fileSize: Int64, duration: TimeInterval, createdAt: Date, videoWidth: Int, videoHeight: Int)`](init%28id:filename:filepath:filesize:duration:createdat:videowidth:videoheight:%29/index.md)
### Instance Properties
- [`let createdAt: Date`](createdat/index.md)
建立時間
- [`let duration: TimeInterval`](duration/index.md)
錄影時長(秒)
- [`let fileName: String`](filename/index.md)
檔案名稱(含副檔名)
- [`let filePath: String`](filepath/index.md)
檔案完整路徑
- [`let fileSize: Int64`](filesize/index.md)
檔案大小(bytes)
- [`var fileURL: URL`](fileurl/index.md)
檔案 URL
- [`let id: String`](id/index.md)
檔案 ID(UUID)
- [`let videoHeight: Int`](videoheight/index.md)
影片高度(pixels,從 mp4 video track naturalSize 取得;無法讀取時為 0)
- [`let videoWidth: Int`](videowidth/index.md)
影片寬度(pixels,從 mp4 video track naturalSize 取得;無法讀取時為 0)
---
# init(id:fileName:filePath:fileSize:duration:createdAt:videoWidth:videoHeight:)
[RevueSDK](../../index.md) › [RecordingFileInfo](../index.md) › init(id:fileName:filePath:fileSize:duration:createdAt:videoWidth:videoHeight:)
*Initializer*
```swift
init(id: String = UUID().uuidString, fileName: String, filePath: String, fileSize: Int64, duration: TimeInterval, createdAt: Date = Date(), videoWidth: Int = 0, videoHeight: Int = 0)
```
---
# videoHeight
[RevueSDK](../../index.md) › [RecordingFileInfo](../index.md) › videoHeight
*Instance Property*
影片高度(pixels,從 mp4 video track naturalSize 取得;無法讀取時為 0)
```swift
let videoHeight: Int
```
---
# videoWidth
[RevueSDK](../../index.md) › [RecordingFileInfo](../index.md) › videoWidth
*Instance Property*
影片寬度(pixels,從 mp4 video track naturalSize 取得;無法讀取時為 0)
```swift
let videoWidth: Int
```
---
# RemoteAudioTrack
[RevueSDK](../index.md) › RemoteAudioTrack
*Class*
遠端音訊 Track — 其他參與者的麥克風聲音
```swift
class RemoteAudioTrack
```
## Overview
```swift
let track = remoteParticipant.firstAudioTrack as? RemoteAudioTrack
track?.volume = 0.5 // 調整收聽音量(0 拒聽、1 原音、最大 10 倍放大)
track?.add(audioRenderer: myRenderer) // 進階:自訂音訊處理
```
## Topics
### Instance Properties
- [`var volume: Double`](volume/index.md)
## Relationships
### Inherits From
- [`AudioTrack`](../audiotrack/index.md)
---
# volume
[RevueSDK](../../index.md) › [RemoteAudioTrack](../index.md) › volume
*Instance Property*
```swift
var volume: Double { get set }
```
## Discussion
- `0.0` 拒聽該參與者:資料仍持續接收、振幅歸零 —— 統計與說話指示不受影響, 分發線(MCU host 轉發混音、錄影)與播放線收到的都是靜音段,錄影音軌連續而該段無聲
- `1.0` 原音(預設)
- `10.0` 放大 10 倍(多路相加可能超出滿刻度,由混音後的限幅器承接)
超出範圍的值會被夾住(`clampVolume`)。
增益在**分岔之前**套用一次(`RemoteParticipant.handleAudioRawDataReceived`:量測之後、 分發線與播放線分岔之前,`applyGain`),所以自己聽到的、MCU host 轉發給 guest 的混音、 錄影檔三處一致;播放線混音器該路的 `playerNode.volume` 不再參與。
寫入端在 main(介面滑桿經跨橋),讀取端在參與者音訊佇列(增益套用),兩條 thread 任意讀寫,故以鎖保護(standard `apple/0003`)。讀取端**讀一次存區域變數**再套用。 非 `@Published`、不觸發 SwiftUI 更新,故寫入無主執行緒限制。
---
# disconnect()
[RevueSDK](../../index.md) › [RemoteParticipant](../index.md) › disconnect()
*Instance Method*
斷開與此參與者的連線
```swift
@MainActor func disconnect()
```
## Discussion
內部透過 ToxAV hangUp 掛斷,並 fire `participantDidDisconnect`(雙 fire ParticipantDelegate + RoomDelegate),設 `lastDisconnectReason = .leave`。 其他參與者不受影響。
```swift
Room.shared.remoteParticipants["bob"]?.disconnect()
```
必須在 main thread 呼叫(`@MainActor`):清理會改 `@Published` 狀態, 事件也在 main 同步送出。
---
# RemoteParticipant
[RevueSDK](../index.md) › RemoteParticipant
*Class*
遠端參與者(其他人)— 可訂閱其 Track、控制連線狀態
```swift
class RemoteParticipant
```
## Overview
透過 `Room.shared.remoteParticipants[friendId]` 取得(對方接受邀請進房後存在)。
每個 RemoteParticipant 有獨立的 `paths`(FriendConnectionPaths bitmap,v0.13.0-rc1 起), 因為 RevueSDK 的連線是針對 friendId(直連架構),不是針對 Room。
```swift
let alice = Room.shared.remoteParticipants["alice"]
// 視訊
videoView.track = alice?.firstCameraVideoTrack
// 斷開連線
alice?.disconnect()
// 邀請流程由 RevueDelegate 處理:
// - 收到邀請:RevueDelegate.didReceiveInvitation
// - 接聽/拒絕:Revue.shared.friends.acceptInvitation / rejectInvitation
// - 通話建立:RoomDelegate.participantDidConnect
// - 通話結束:RoomDelegate.participantDidDisconnect(讀 lastDisconnectReason)
```
## Topics
### Instance Properties
- [`var lastDisconnectReason: DisconnectReason?`](lastdisconnectreason/index.md)
最近一次斷線原因
- [`var paths: FriendConnectionPaths`](paths/index.md)
與此參與者的連線路徑(FriendConnectionPaths bitmap,v0.13.0-rc1)
### Instance Methods
- [`func disconnect()`](disconnect%28%29/index.md)
斷開與此參與者的連線
- [`func requestMute(Bool) async throws -> Bool`](requestmute%28_:%29/index.md)
發送靜音請求給此參與者
## Relationships
### Inherits From
- [`Participant`](../participant/index.md)
---
# lastDisconnectReason
[RevueSDK](../../index.md) › [RemoteParticipant](../index.md) › lastDisconnectReason
*Instance Property*
最近一次斷線原因
```swift
var lastDisconnectReason: DisconnectReason? { get }
```
## Discussion
在 `participantDidDisconnect` callback fire 前由 SDK 設好。 App 可在 callback 內讀此屬性區分對方主動掛斷或網路斷線。
---
# paths
[RevueSDK](../../index.md) › [RemoteParticipant](../index.md) › paths
*Instance Property*
與此參與者的連線路徑(FriendConnectionPaths bitmap,v0.13.0-rc1)
```swift
@Published var paths: FriendConnectionPaths { get set }
```
## Discussion
事件:
- 一律 fire `RevueDelegate.didUpdatePaths`(由 Revue 層 fire)
- 同時 fire `RoomDelegate.didUpdatePaths` 與 `ParticipantDelegate.didUpdatePaths`, **含空 bitmap**
- paths 變空**不代表**通話結束、不會移除參與者:C 層仍在重連,網路恢復後 paths 自動回報 有連線。上層應顯示「連線中斷」並保留掛斷入口;通話真正結束時才收到 `participantDidDisconnect`
---
# requestMute(_:)
[RevueSDK](../../index.md) › [RemoteParticipant](../index.md) › requestMute(_:)
*Instance Method*
發送靜音請求給此參與者
```swift
@discardableResult func requestMute(_ muted: Bool) async throws -> Bool
```
## Parameters
- muted
true = 請求靜音, false = 請求取消靜音
## Return Value
送達時(對方裝置 toxcore 回 ACK)為 true;送不到時 throw,不回 false
## Discussion
這是「通知」而非「強制」:只送給這一位(不是廣播),對方收到 `didReceiveMuteRequest` callback 後,由對方的 App 決定是否配合(能力層 vs 授權層分離)。
與本地調整收聽音量的差異:
- `RemoteAudioTrack.volume = 0`:本地端不再播放對方的聲音(只影響自己,對方不知道)
- `requestMute(true)`:通知對方,請對方自己靜音(有網路延遲,對方可不配合)
常見組合使用:
```swift
// 情境 1:我不想聽 A 說話(只影響自己)
(remoteA?.firstAudioTrack as? RemoteAudioTrack)?.volume = 0
// 情境 2:主持人要求 A 靜音(通知 + 立即本地不播放)
func hostMuteParticipant(_ participant: RemoteParticipant) async throws {
(participant.firstAudioTrack as? RemoteAudioTrack)?.volume = 0 // 立即:我這邊不播放
try await participant.requestMute(true) // 通知:請對方靜音
}
// 情境 3:只是通知對方(不強制)
try await remoteA?.requestMute(true)
```
> **Throws**
>
> - `RoomMessageError.encodingFailed` — JSON envelope 編碼失敗(罕見)
> - `RoomMessageError.messageTooLong` — envelope 超過 toxcore 1372 bytes 上限
> - `RoomMessageError.transportFailed(underlying:)` — 底層 FriendManager 失敗
> - `CancellationError` — Revue.destroy() 觸發
---
# RemoteTrackPublication
[RevueSDK](../index.md) › RemoteTrackPublication
*Class*
遠端 Track 發佈狀態
```swift
class RemoteTrackPublication
```
## Overview
透過 `remoteParticipant.trackPublications` 取得。
## Topics
### Instance Properties
- [`var isEnabled: Bool`](isenabled/index.md)
是否啟用接收
### Instance Methods
- [`func set(enabled: Bool)`](set%28enabled:%29/index.md)
設定是否啟用接收
## Relationships
### Inherits From
- [`TrackPublication`](../trackpublication/index.md)
---
# isEnabled
[RevueSDK](../../index.md) › [RemoteTrackPublication](../index.md) › isEnabled
*Instance Property*
是否啟用接收
```swift
@Published var isEnabled: Bool { get set }
```
---
# set(enabled:)
[RevueSDK](../../index.md) › [RemoteTrackPublication](../index.md) › set(enabled:)
*Instance Method*
設定是否啟用接收
```swift
func set(enabled: Bool)
```
## Discussion
`enabled=false` 時本地不處理收到的資料(省 CPU,但資料仍在傳)。 `enabled=true` 時恢復處理。
---
# RemoteVideoTrack
[RevueSDK](../index.md) › RemoteVideoTrack
*Class*
遠端視訊 Track — 其他參與者的相機或螢幕分享畫面
```swift
class RemoteVideoTrack
```
## Overview
透過 `remoteParticipant.firstCameraVideoTrack` 取得, 再綁定到 `VideoView` 顯示。
```swift
let track = remoteParticipant.firstCameraVideoTrack
videoView.track = track
```
## Relationships
### Inherits From
- [`VideoTrack`](../videotrack/index.md)
---
# address
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.BootstrapNode](../index.md) › address
*Instance Property*
```swift
let address: String
```
---
# Revue.BootstrapNode
[RevueSDK](../../index.md) › [Revue](../index.md) › Revue.BootstrapNode
*Structure*
Bootstrap 節點
```swift
nonisolated struct BootstrapNode
```
## Topics
### Initializers
- [`init(address: String, port: Int32, publicKey: String, tcpRelayPort: Int32?, udpRelayPort: Int32?)`](init%28address:port:publickey:tcprelayport:udprelayport:%29/index.md)
### Instance Properties
- [`let address: String`](address/index.md)
- [`let port: Int32`](port/index.md)
DHT bootstrap 埠號,一定會註冊
- [`let publicKey: String`](publickey/index.md)
- [`let tcpRelayPort: Int32?`](tcprelayport/index.md)
TCP relay 埠號;nil = 此節點不註冊 TCP relay
- [`let udpRelayPort: Int32?`](udprelayport/index.md)
UDP relay 埠號;nil = 此節點不註冊 UDP relay(另受 `NetworkOptions.enableUdpRelay` 控制)
---
# init(address:port:publicKey:tcpRelayPort:udpRelayPort:)
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.BootstrapNode](../index.md) › init(address:port:publicKey:tcpRelayPort:udpRelayPort:)
*Initializer*
```swift
init(address: String, port: Int32 = 33445, publicKey: String, tcpRelayPort: Int32? = nil, udpRelayPort: Int32? = nil)
```
---
# port
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.BootstrapNode](../index.md) › port
*Instance Property*
DHT bootstrap 埠號,一定會註冊
```swift
let port: Int32
```
---
# publicKey
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.BootstrapNode](../index.md) › publicKey
*Instance Property*
```swift
let publicKey: String
```
---
# tcpRelayPort
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.BootstrapNode](../index.md) › tcpRelayPort
*Instance Property*
TCP relay 埠號;nil = 此節點不註冊 TCP relay
```swift
let tcpRelayPort: Int32?
```
---
# udpRelayPort
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.BootstrapNode](../index.md) › udpRelayPort
*Instance Property*
UDP relay 埠號;nil = 此節點不註冊 UDP relay(另受 `NetworkOptions.enableUdpRelay` 控制)
```swift
let udpRelayPort: Int32?
```
---
# bootstrapNodes
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.Configuration](../index.md) › bootstrapNodes
*Instance Property*
Bootstrap 節點列表
```swift
var bootstrapNodes: [BootstrapNode]
```
---
# default
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.Configuration](../index.md) › default
*Type Property*
預設設定(**不含 bootstrap 節點**)
```swift
static var `default`: Configuration { get }
```
## Discussion
SDK 不再內建 bootstrap 節點清單 — 由上層 App 自己決定使用哪些節點。 取得 `default` 之後仍需設定 `bootstrapNodes`,否則 DHT 無法 bootstrap。
---
# Revue.Configuration
[RevueSDK](../../index.md) › [Revue](../index.md) › Revue.Configuration
*Structure*
SDK 初始化設定
```swift
nonisolated struct Configuration
```
## Topics
### Initializers
- [`init(savedataFileName: String?, savedataPassword: String?, bootstrapNodes: [BootstrapNode], networkOptions: NetworkOptions, licenseData: Data?, mqttOptions: MqttOptions?)`](init%28savedatafilename:savedatapassword:bootstrapnodes:networkoptions:licensedata:mqttoptions:%29/index.md)
### Instance Properties
- [`var bootstrapNodes: [BootstrapNode]`](bootstrapnodes/index.md)
Bootstrap 節點列表
- [`var licenseData: Data?`](licensedata/index.md)
授權憑證位元組(v0.14.0 起必要)
- [`var mqttOptions: MqttOptions?`](mqttoptions/index.md)
MQTT / presence connection settings. nil = core built-in broker and credentials. Read only at initialize; `Revue.startPresence(_:)` changes the settings later.
- [`var networkOptions: NetworkOptions`](networkoptions/index.md)
網路選項
- [`var savedataFileName: String?`](savedatafilename/index.md)
Savedata 檔名(不含路徑;Bridge 自動放在 ApplicationSupport 內)
- [`var savedataPassword: String?`](savedatapassword/index.md)
存檔加密密碼(可選) 若設定則使用加密存檔初始化
### Type Properties
- [`static var `default`: Configuration`](default/index.md)
預設設定(**不含 bootstrap 節點**)
---
# init(savedataFileName:savedataPassword:bootstrapNodes:networkOptions:licenseData:mqttOptions:)
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.Configuration](../index.md) › init(savedataFileName:savedataPassword:bootstrapNodes:networkOptions:licenseData:mqttOptions:)
*Initializer*
```swift
init(savedataFileName: String? = nil, savedataPassword: String? = nil, bootstrapNodes: [BootstrapNode], networkOptions: NetworkOptions = .default, licenseData: Data? = nil, mqttOptions: MqttOptions? = nil)
```
---
# licenseData
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.Configuration](../index.md) › licenseData
*Instance Property*
授權憑證位元組(v0.14.0 起必要)
```swift
var licenseData: Data?
```
## Discussion
- `revuekit_init` 為 fail-closed 授權;nil 或空 → `initialize` throw `.licenseMissing`
- 由上層負責讀取 `.lic` 檔為 `Data`(bundle / assets / 其他來源皆可)後傳入
---
# mqttOptions
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.Configuration](../index.md) › mqttOptions
*Instance Property*
MQTT / presence connection settings. nil = core built-in broker and credentials. Read only at initialize; `Revue.startPresence(_:)` changes the settings later.
```swift
var mqttOptions: MqttOptions?
```
---
# networkOptions
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.Configuration](../index.md) › networkOptions
*Instance Property*
網路選項
```swift
var networkOptions: NetworkOptions
```
---
# savedataFileName
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.Configuration](../index.md) › savedataFileName
*Instance Property*
Savedata 檔名(不含路徑;Bridge 自動放在 ApplicationSupport 內)
```swift
var savedataFileName: String?
```
## Discussion
- nil:使用 SDK 預設檔名 `revue.bin`
- 檔名不可包含 `/` 或 `..`(會被 Bridge 拒絕、初始化失敗)
---
# savedataPassword
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.Configuration](../index.md) › savedataPassword
*Instance Property*
存檔加密密碼(可選) 若設定則使用加密存檔初始化
```swift
var savedataPassword: String?
```
---
# connectionState
[RevueSDK](../../index.md) › [Revue](../index.md) › connectionState
*Instance Property*
自身 DHT 連線狀態
```swift
@Published @MainActor var connectionState: SelfConnectionStatus { get set }
```
---
# delegates
[RevueSDK](../../index.md) › [Revue](../index.md) › delegates
*Instance Property*
多播代理(朋友層事件)
```swift
@MainActor let delegates: MulticastDelegate
```
---
# destroy(_:)
[RevueSDK](../../index.md) › [Revue](../index.md) › destroy(_:)
*Instance Method*
銷毀 SDK(離開房間 + 清除 delegates + 停止 core loop + 銷毀 C 實例), 再依 `options` 刪除 SDK 寫到硬碟的資料
```swift
@MainActor func destroy(_ options: DestroyOptions = .default) async
```
## Discussion
- 有執行中的 instance:走完整銷毀流程,C 層銷毀之後才刪除(C 層存活時會自動存檔寫回)
- 沒有 instance(例如存檔損壞、授權過期導致 initialize 失敗):跳過整段銷毀流程, 只做 `options` 指定的刪除 —— 銷毀流程會碰已失效的 C handle,沒有 instance 時執行有閃退風險
刪除失敗只寫 log、不丟錯。預設 `.default` 什麼都不刪。
v0.14.0:內部會自動解除 frame crypto(等同呼叫 `clearFrameCryptor()`)—— 有包版 `setFrameCryptor(_:)` 的 cryptor 物件會被自動 release,故銷毀前**不需**手動 clear。 ⚠️ 直通版 `setFrameCrypto(...)` 使用者自己 retain 的 `userData` **不在此範圍**,仍須自行 release。
---
# default
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.DestroyOptions](../index.md) › default
*Type Property*
什麼都不刪(既有的 destroy 行為)
```swift
static var `default`: DestroyOptions { get }
```
---
# deleteAllData
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.DestroyOptions](../index.md) › deleteAllData
*Type Property*
刪除身分存檔、core 附屬檔與所有錄影
```swift
static var deleteAllData: DestroyOptions { get }
```
---
# deleteRecordings
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.DestroyOptions](../index.md) › deleteRecordings
*Instance Property*
刪除錄影目錄下的所有錄影
```swift
var deleteRecordings: Bool
```
---
# deleteSavedata
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.DestroyOptions](../index.md) › deleteSavedata
*Instance Property*
刪除本次 initialize 用的身分存檔與 core 附屬檔(`<檔名>_friends.dat`、`traffic_stats/`)
```swift
var deleteSavedata: Bool
```
## Discussion
- 只刪本次 initialize 的檔名:SDK 不記得以前用過的檔名
- 授權高水位檔 `<存檔>.rklic_hwm` 一律保留(綁裝置、不綁身分)
- 本次從未進到 initialize(存檔路徑未知)則略過
---
# Revue.DestroyOptions
[RevueSDK](../../index.md) › [Revue](../index.md) › Revue.DestroyOptions
*Structure*
`destroy(_:)` 的選項:關閉 SDK 時一併刪除哪些 SDK 寫到硬碟的資料
```swift
nonisolated struct DestroyOptions
```
## Overview
刪除一律在 C 層銷毀之後才執行(C 層存活時會自動存檔寫回),順序由 `destroy` 保證。 刪除失敗只寫 log,`destroy` 不丟錯。
## Topics
### Initializers
- [`init(deleteSavedata: Bool, deleteRecordings: Bool)`](init%28deletesavedata:deleterecordings:%29/index.md)
### Instance Properties
- [`var deleteRecordings: Bool`](deleterecordings/index.md)
刪除錄影目錄下的所有錄影
- [`var deleteSavedata: Bool`](deletesavedata/index.md)
刪除本次 initialize 用的身分存檔與 core 附屬檔(`<檔名>_friends.dat`、`traffic_stats/`)
### Type Properties
- [`static var `default`: DestroyOptions`](default/index.md)
什麼都不刪(既有的 destroy 行為)
- [`static var deleteAllData: DestroyOptions`](deletealldata/index.md)
刪除身分存檔、core 附屬檔與所有錄影
---
# init(deleteSavedata:deleteRecordings:)
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.DestroyOptions](../index.md) › init(deleteSavedata:deleteRecordings:)
*Initializer*
```swift
init(deleteSavedata: Bool = false, deleteRecordings: Bool = false)
```
---
# friends
[RevueSDK](../../index.md) › [Revue](../index.md) › friends
*Instance Property*
好友管理器
```swift
@MainActor let friends: FriendManager
```
---
# getCoreVersion()
[RevueSDK](../../index.md) › [Revue](../index.md) › getCoreVersion()
*Type Method*
取得 revuekit-c 核心版本字串(例:「0.11.0」)
```swift
nonisolated static func getCoreVersion() -> String
```
---
# getVersion()
[RevueSDK](../../index.md) › [Revue](../index.md) › getVersion()
*Type Method*
取得自己版本
```swift
nonisolated static func getVersion() -> String
```
---
# Revue
[RevueSDK](../index.md) › Revue
*Class*
朋友層 singleton — SDK 入口點,管理好友關係、事件輪詢、delegate 推送
```swift
@MainActor class Revue
```
## Overview
Revue.shared 是生命週期最長的 singleton(App 啟動就存在), 擁有 core loop(`coreQueue` serial queue)—— 單一迴圈依序驅動 `revuekit_iterate` 與事件輪詢/路由,間隔由 `revuekit_iteration_interval()` 動態決定。`destroy` 亦排在同一條 queue,確保與 iterate 不重疊(toxcore 契約: 同一 instance 任何時刻最多一個 API 函式運作)。
所有方法和屬性必須在 Main Thread 存取(@MainActor 強制)。
## Topics
### Structures
- [`struct BootstrapNode`](bootstrapnode/index.md)
Bootstrap 節點
- [`struct Configuration`](configuration/index.md)
SDK 初始化設定
- [`struct DestroyOptions`](destroyoptions/index.md)
`destroy(_:)` 的選項:關閉 SDK 時一併刪除哪些 SDK 寫到硬碟的資料
- [`struct MqttOptions`](mqttoptions/index.md)
MQTT / presence connection settings (mirrors core `RevueKitMqttOptions`; same shape as Android `MqttOptions`).
- [`struct NetworkOptions`](networkoptions/index.md)
網路選項
### Instance Properties
- [`var connectionState: SelfConnectionStatus`](connectionstate/index.md)
自身 DHT 連線狀態
- [`let delegates: MulticastDelegate`](delegates/index.md)
多播代理(朋友層事件)
- [`let friends: FriendManager`](friends/index.md)
好友管理器
- [`var logLevel: Set`](loglevel/index.md)
The SDK log levels selected now.
- [`var logPublisher: AnyPublisher`](logpublisher/index.md)
SDK log 流(v0.13.0-rc1 新增)
- [`let presence: PresenceManager`](presence/index.md)
Presence 管理器
- [`var revueAddress: String`](revueaddress/index.md)
自身 Revue Address(用於加好友,初始化後可用)
- [`var revueId: String`](revueid/index.md)
自身 Revue ID(Base58 公鑰,初始化後可用)
- [`var selfName: String`](selfname/index.md)
自身名稱(初始化後可用)
- [`var selfStatus: UserStatus`](selfstatus/index.md)
自身使用者狀態(初始化後可用)
- [`var selfStatusMessage: String`](selfstatusmessage/index.md)
自身狀態訊息(初始化後可用)
### Instance Methods
- [`func destroy(DestroyOptions) async`](destroy%28_:%29/index.md)
銷毀 SDK(離開房間 + 清除 delegates + 停止 core loop + 銷毀 C 實例), 再依 `options` 刪除 SDK 寫到硬碟的資料
- [`func initialize(Revue.Configuration) throws`](initialize%28_:%29/index.md)
初始化 SDK(建立 C 實例 + bootstrap + 啟動 core loop)
- [`func setLogLevel(Set)`](setloglevel%28_:%29/index.md)
Choose which SDK log levels are output. Takes effect at once.
- [`func setSelfName(String) -> Bool`](setselfname%28_:%29/index.md)
設定自身名稱
- [`func setSelfStatus(UserStatus) -> Bool`](setselfstatus%28_:%29/index.md)
設定自身使用者狀態(UserStatus:none / away / busy)
- [`func setSelfStatusMessage(String) -> Bool`](setselfstatusmessage%28_:%29/index.md)
設定自身狀態訊息(個人簽名)
- [`func startPresence(MqttOptions?) async throws`](startpresence%28_:%29/index.md)
Start presence, or replace the running connection with new settings.
- [`func stopPresence() async`](stoppresence%28%29/index.md)
Stop presence, for example when the user signs out. Calls and friend connections are not affected.
### Type Properties
- [`static let shared: Revue`](shared/index.md)
### Type Methods
- [`static func getCoreVersion() -> String`](getcoreversion%28%29/index.md)
取得 revuekit-c 核心版本字串(例:「0.11.0」)
- [`static func getVersion() -> String`](getversion%28%29/index.md)
取得自己版本
---
# initialize(_:)
[RevueSDK](../../index.md) › [Revue](../index.md) › initialize(_:)
*Instance Method*
初始化 SDK(建立 C 實例 + bootstrap + 啟動 core loop)
```swift
@MainActor func initialize(_ config: Revue.Configuration) throws
```
## Discussion
v0.13.0-rc1:合併原 `configure(_:) + initialize()` 兩段式 API 為單一呼叫, 直接傳入 `Configuration`,移除「忘了呼叫 configure」的失敗路徑。
v0.14.0:`revuekit_init` 為 fail-closed 授權 — `config.licenseData` 缺失或憑證無效/過期時 throw `RevueInitError`(license 四種原因區分)。上層可對「憑證過期」等分別提示。
> **Throws**
>
> `RevueInitError`(`.licenseMissing/.licenseMalformed/.licenseInvalid/.licenseExpired` 或 `.invalidConfiguration/.initFailed(code:)`)
---
# logLevel
[RevueSDK](../../index.md) › [Revue](../index.md) › logLevel
*Instance Property*
The SDK log levels selected now.
```swift
nonisolated var logLevel: Set { get }
```
---
# logPublisher
[RevueSDK](../../index.md) › [Revue](../index.md) › logPublisher
*Instance Property*
SDK log 流(v0.13.0-rc1 新增)
```swift
nonisolated var logPublisher: AnyPublisher { get }
```
## Discussion
含 C 核心層 log(revuekit / toxcore / mqtt / presence 等)+ SDK 內部 Swift log(透過 `revueLog()` helper)。 已自動 hop 到 main thread、訂閱者不需要再 `.receive(on:)`。
設計:spec doc_superpowers/specs/2026-06-29-log-config-bridge-design.md
```swift
Revue.shared.logPublisher
.filter { $0.level >= .warning }
.sink { entry in
print("[\(entry.level.name)] [\(entry.source)] \(entry.message)")
}
.store(in: &cancellables)
```
---
# brokerUrl
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.MqttOptions](../index.md) › brokerUrl
*Instance Property*
```swift
var brokerUrl: String?
```
---
# caCertPath
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.MqttOptions](../index.md) › caCertPath
*Instance Property*
```swift
var caCertPath: String?
```
---
# deferred
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.MqttOptions](../index.md) › deferred
*Type Property*
Settings for an App that has no MQTT account yet: `initialize` does not start presence. Call `Revue.startPresence(_:)` after the user signs in.
```swift
static var deferred: MqttOptions { get }
```
---
# deferStart
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.MqttOptions](../index.md) › deferStart
*Instance Property*
```swift
var deferStart: Bool
```
---
# Revue.MqttOptions
[RevueSDK](../../index.md) › [Revue](../index.md) › Revue.MqttOptions
*Structure*
MQTT / presence connection settings (mirrors core `RevueKitMqttOptions`; same shape as Android `MqttOptions`).
```swift
nonisolated struct MqttOptions
```
## Overview
- `brokerUrl`: scheme must be tcp / ssl / tls / mqtts / ws / wss, e.g. `wss://www.arctos.tw/mqtt`; nil = the core built-in broker (`wss://www.arctos.tw/mqtt`). Invalid values make `initialize` throw `.invalidConfiguration`.
- `username` / `password`: nil = core built-in credentials.
- `caCertPath`: PEM CA bundle path supplied by the App; nil = no server certificate verification (the core logs a warning for TLS brokers). Used only for TLS schemes.
- `presenceQos`: QoS for presence publish/subscribe/LWT, 0-2, default 1; out of range throws `.invalidConfiguration`.
- `deferStart`: true = `initialize` does not start presence: no MQTT client is created, the broker is never contacted and no presence events are delivered. The other fields are ignored while deferred. Use [`deferred`](deferred/index.md) for this case and call `Revue.startPresence(_:)` later with the real settings. It applies to one `initialize` only. `startPresence(_:)` ignores this field.
## Topics
### Initializers
- [`init(brokerUrl: String?, username: String?, password: String?, caCertPath: String?, presenceQos: Int, deferStart: Bool)`](init%28brokerurl:username:password:cacertpath:presenceqos:deferstart:%29/index.md)
### Instance Properties
- [`var brokerUrl: String?`](brokerurl/index.md)
- [`var caCertPath: String?`](cacertpath/index.md)
- [`var deferStart: Bool`](deferstart/index.md)
- [`var password: String?`](password/index.md)
- [`var presenceQos: Int`](presenceqos/index.md)
- [`var username: String?`](username/index.md)
### Type Properties
- [`static var deferred: MqttOptions`](deferred/index.md)
Settings for an App that has no MQTT account yet: `initialize` does not start presence. Call `Revue.startPresence(_:)` after the user signs in.
---
# init(brokerUrl:username:password:caCertPath:presenceQos:deferStart:)
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.MqttOptions](../index.md) › init(brokerUrl:username:password:caCertPath:presenceQos:deferStart:)
*Initializer*
```swift
init(brokerUrl: String? = nil, username: String? = nil, password: String? = nil, caCertPath: String? = nil, presenceQos: Int = 1, deferStart: Bool = false)
```
---
# password
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.MqttOptions](../index.md) › password
*Instance Property*
```swift
var password: String?
```
---
# presenceQos
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.MqttOptions](../index.md) › presenceQos
*Instance Property*
```swift
var presenceQos: Int
```
---
# username
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.MqttOptions](../index.md) › username
*Instance Property*
```swift
var username: String?
```
---
# default
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › default
*Type Property*
預設網路選項
```swift
static var `default`: NetworkOptions { get }
```
---
# enableTcpRelay
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › enableTcpRelay
*Instance Property*
啟用 TCP 中繼
```swift
var enableTcpRelay: Bool
```
## Discussion
> **Note**
>
> `false` 只 deny **per-friend path**,**不影響 relay 節點的註冊** —— TCP relay pool 是全域共享資源,同時服務 self DHT 與 onion,擋掉註冊會讓 UDP 受阻環境連 DHT 都上不去。故 `BootstrapNode.tcpRelayPort` / savedata 兩個註冊入口行為與 0.17 完全相同。
>
> 代價是 relay pool 的供給面變窄(經好友層的兩條供給被切斷),設 `false` 的整合方**應為足夠節點填 `BootstrapNode.tcpRelayPort`**,否則 bootstrap node 全掛時就沒有 TCP fallback。
---
# enableUdpDirect
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › enableUdpDirect
*Instance Property*
啟用 UDP 直連(P2P hole punching)
```swift
var enableUdpDirect: Bool
```
## Discussion
> **Note**
>
> core 0.18.0 的 hard deny 對本 flag **尚未完整生效**(Phase 1 範圍 只含兩個 relay flag)。設 `false` 目前仍維持 0.17 行為:只擋出向、入向照收。 入向拒收排入 Phase 2(direct 與 DHT / onion 共用同一個 UDP socket)。
---
# enableUdpRelay
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › enableUdpRelay
*Instance Property*
啟用 UDP 中繼
```swift
var enableUdpRelay: Bool
```
## Discussion
> **Note**
>
> 與 [`enableTcpRelay`](../enabletcprelay/index.md) 不同,UDP relay 只服務 friend path,故 `false` 時**連註冊都擋**:C 層 `revuekit_add_udp_relay` 回 `REVUEKIT_ERR_TRANSPORT_DISABLED`(-31)(0.17 為靜默成功、形同誤啟用)。 SDK 內部據此在 `initialize` / reconnect 的 bootstrap 迴圈跳過 UDP relay 註冊。
---
# endPort
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › endPort
*Instance Property*
端口範圍結束(0 = 使用 toxcore 預設 33545)
```swift
var endPort: UInt16
```
---
# Revue.NetworkOptions
[RevueSDK](../../index.md) › [Revue](../index.md) › Revue.NetworkOptions
*Structure*
網路選項
```swift
nonisolated struct NetworkOptions
```
## Transport flag 的 hard deny 語意(core 0.18.0 起,issue #102)
下方三個 transport flag 設 `false` 的語意在 0.18.0 擴張為**雙向封鎖**: 除了本端不主動走該路徑,**對端經該路徑送來的封包也會在解密前丟棄**, 且 [`RemoteParticipant`](../../remoteparticipant/index.md) 的 `paths` bitmap 對應位元**恆為 0**、不再產生該 path 的狀態變化事件。0.17 及之前只擋出向、入向照收(還會把該位元點亮)。
三者皆為 **init-time 唯讀**:無 runtime setter,要改必須 `destroy()` 後重新 `initialize(_:)`。
⚠️ **雙方 flag 逐位 AND 為全 0 的一對使用者永久不可達**,且 SDK 不提供專屬 事件或錯誤碼 —— 表現為「對方永遠離線」。升級時應全 fleet 先升上 0.18.0 並 維持三旗全開,確認升完後再逐一收緊。
⚠️ 兩種**矛盾組合會讓 `initialize(_:)` 直接失敗**(C 層回 `REVUEKIT_ERR_INVALID_TRANSPORT_CONFIG`(-32),不建立任何 instance): ① 三個 flag 全 `false`;② `switchAEnabled == true` 且兩個 relay flag 皆 `false`。
## Topics
### Initializers
- [`init(enableUdpDirect: Bool, enableTcpRelay: Bool, enableUdpRelay: Bool, ipv6Enabled: Bool, localDiscoveryEnabled: Bool, startPort: UInt16, endPort: UInt16, switchAEnabled: Bool, switchBEnabled: Bool, switchAGracePeriodMs: UInt32, switchBGracePeriodMs: UInt32)`](init%28enableudpdirect:enabletcprelay:enableudprelay:ipv6enabled:localdiscoveryenabled:startport:endport:switchaenabled:switchbenabled:switchagraceperiodms:switchbgraceperiodms:%29/index.md)
### Instance Properties
- [`var enableTcpRelay: Bool`](enabletcprelay/index.md)
啟用 TCP 中繼
- [`var enableUdpDirect: Bool`](enableudpdirect/index.md)
啟用 UDP 直連(P2P hole punching)
- [`var enableUdpRelay: Bool`](enableudprelay/index.md)
啟用 UDP 中繼
- [`var endPort: UInt16`](endport/index.md)
端口範圍結束(0 = 使用 toxcore 預設 33545)
- [`var ipv6Enabled: Bool`](ipv6enabled/index.md)
啟用 IPv6
- [`var localDiscoveryEnabled: Bool`](localdiscoveryenabled/index.md)
啟用區網探索
- [`var startPort: UInt16`](startport/index.md)
端口範圍起始(0 = 使用 toxcore 預設 33445)
- [`var switchAEnabled: Bool`](switchaenabled/index.md)
Switch A:直連切換遲滯保護(v0.15.0)
- [`var switchAGracePeriodMs: UInt32`](switchagraceperiodms/index.md)
Switch A 寬限期(毫秒)
- [`var switchBEnabled: Bool`](switchbenabled/index.md)
Switch B:直連可見性寬限期(v0.15.0)
- [`var switchBGracePeriodMs: UInt32`](switchbgraceperiodms/index.md)
Switch B 寬限期(毫秒),語意同 `switchAGracePeriodMs`,與 Switch A 各自獨立計時
### Type Properties
- [`static var `default`: NetworkOptions`](default/index.md)
預設網路選項
---
# init(enableUdpDirect:enableTcpRelay:enableUdpRelay:ipv6Enabled:localDiscoveryEnabled:startPort:endPort:switchAEnabled:switchBEnabled:switchAGracePeriodMs:switchBGracePeriodMs:)
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › init(enableUdpDirect:enableTcpRelay:enableUdpRelay:ipv6Enabled:localDiscoveryEnabled:startPort:endPort:switchAEnabled:switchBEnabled:switchAGracePeriodMs:switchBGracePeriodMs:)
*Initializer*
```swift
init(enableUdpDirect: Bool = true, enableTcpRelay: Bool = true, enableUdpRelay: Bool = true, ipv6Enabled: Bool = true, localDiscoveryEnabled: Bool = true, startPort: UInt16 = 0, endPort: UInt16 = 0, switchAEnabled: Bool = false, switchBEnabled: Bool = false, switchAGracePeriodMs: UInt32 = 0, switchBGracePeriodMs: UInt32 = 0)
```
---
# ipv6Enabled
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › ipv6Enabled
*Instance Property*
啟用 IPv6
```swift
var ipv6Enabled: Bool
```
---
# localDiscoveryEnabled
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › localDiscoveryEnabled
*Instance Property*
啟用區網探索
```swift
var localDiscoveryEnabled: Bool
```
---
# startPort
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › startPort
*Instance Property*
端口範圍起始(0 = 使用 toxcore 預設 33445)
```swift
var startPort: UInt16
```
---
# switchAEnabled
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › switchAEnabled
*Instance Property*
Switch A:直連切換遲滯保護(v0.15.0)
```swift
var switchAEnabled: Bool
```
## Discussion
關閉(預設)時仍會依固定優先序 `UDP_DIRECT > UDP_RELAY > TCP_RELAY` 切換路徑, 只要直連 ALIVE 就用、不管穩不穩;開啟則加上遲滯 —— 直連掉線立即改走中繼, 但直連恢復需連續穩定一段時間才切回,避免不穩的直連造成通話抖動。 **僅影響音視訊(lossy media)**,文字訊息不受影響。init-time-only。
---
# switchAGracePeriodMs
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › switchAGracePeriodMs
*Instance Property*
Switch A 寬限期(毫秒)
```swift
var switchAGracePeriodMs: UInt32
```
## Discussion
0 = 使用 C 預設 15000;非 0 且 < 15000 為非法值, `Revue.initialize` 會 throw `RevueInitError.gracePeriodTooShort`(C -27)。
---
# switchBEnabled
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › switchBEnabled
*Instance Property*
Switch B:直連可見性寬限期(v0.15.0)
```swift
var switchBEnabled: Bool
```
## Discussion
開啟後,剛連線的寬限期內好友需建立 UDP 直連才會顯示上線並可通話; 期間撥號 / 接聽會回 `FriendCallError.noDirectPath`(C -26)。 本質是「延後」而非「禁止」—— 直連建立即刻解除,寬限期逾時仍會以中繼接通。init-time-only。
---
# switchBGracePeriodMs
[RevueSDK](../../../index.md) › [Revue](../../index.md) › [Revue.NetworkOptions](../index.md) › switchBGracePeriodMs
*Instance Property*
Switch B 寬限期(毫秒),語意同 `switchAGracePeriodMs`,與 Switch A 各自獨立計時
```swift
var switchBGracePeriodMs: UInt32
```
---
# objectWillChange
[RevueSDK](../../index.md) › [Revue](../index.md) › objectWillChange
*Instance Property*
- iOS 13.0+
- iPadOS 13.0+
- macCatalyst 13.0+
- macOS 10.15+
- tvOS 13.0+
- watchOS 6.0+
```swift
var objectWillChange: ObservableObjectPublisher { get }
```
---
# ObservableObject Implementations
[RevueSDK](../../index.md) › [Revue](../index.md) › ObservableObject Implementations
*Article*
---
# presence
[RevueSDK](../../index.md) › [Revue](../index.md) › presence
*Instance Property*
Presence 管理器
```swift
@MainActor let presence: PresenceManager
```
## Discussion
對外開放 4 個查詢/設定 API(setStatus / getStatus / getFriendStatus / getConnectionState) 與 friends 同層暴露。SDK 內部負責 MQTT signal 加速層生命週期(reconcile / stop / handleSignalMessage),由 Revue.initialize / destroy / routeEvent 接管。
---
# revueAddress
[RevueSDK](../../index.md) › [Revue](../index.md) › revueAddress
*Instance Property*
自身 Revue Address(用於加好友,初始化後可用)
```swift
@MainActor var revueAddress: String { get }
```
---
# revueId
[RevueSDK](../../index.md) › [Revue](../index.md) › revueId
*Instance Property*
自身 Revue ID(Base58 公鑰,初始化後可用)
```swift
@MainActor var revueId: String { get }
```
---
# selfName
[RevueSDK](../../index.md) › [Revue](../index.md) › selfName
*Instance Property*
自身名稱(初始化後可用)
```swift
@MainActor var selfName: String { get }
```
---
# selfStatus
[RevueSDK](../../index.md) › [Revue](../index.md) › selfStatus
*Instance Property*
自身使用者狀態(初始化後可用)
```swift
@MainActor var selfStatus: UserStatus { get }
```
---
# selfStatusMessage
[RevueSDK](../../index.md) › [Revue](../index.md) › selfStatusMessage
*Instance Property*
自身狀態訊息(初始化後可用)
```swift
@MainActor var selfStatusMessage: String { get }
```
---
# setLogLevel(_:)
[RevueSDK](../../index.md) › [Revue](../index.md) › setLogLevel(_:)
*Instance Method*
Choose which SDK log levels are output. Takes effect at once.
```swift
nonisolated func setLogLevel(_ levels: Set)
```
## Discussion
Covers the network core (revuekit / toxcore / mqtt / presence) and the SDK’s own log lines. This is a selection, not a threshold: `[.warning]` does not include `.error`. An empty set turns all SDK output off. The default is `[.warning, .error]`.
The setting belongs to the process: it can be called before `initialize(_:)`, and neither `initialize(_:)` nor `destroy(_:)` changes it. Any thread may call it.
On iOS a running Broadcast Upload Extension follows the selection too, if the app has an App Group. Without one, the extension logs `[.warning, .error]`.
```swift
Revue.shared.setLogLevel([.debug, .error])
```
---
# setSelfName(_:)
[RevueSDK](../../index.md) › [Revue](../index.md) › setSelfName(_:)
*Instance Method*
設定自身名稱
```swift
@discardableResult @MainActor func setSelfName(_ name: String) -> Bool
```
## Return Value
是否成功(C 層 ACK)
## Discussion
透過 toxcore P2P 廣播給所有好友。本地成功後同步更新 `Room.shared.localParticipant.name`,並觸發:
- `RevueDelegate.didUpdateName`(自己的 identity)
- `RoomDelegate.didUpdateName`(participant = localParticipant)
toxcore 的 self-set 不會回事件給自己,所以 delegate 必須主動觸發。
---
# setSelfStatus(_:)
[RevueSDK](../../index.md) › [Revue](../index.md) › setSelfStatus(_:)
*Instance Method*
設定自身使用者狀態(UserStatus:none / away / busy)
```swift
@discardableResult @MainActor func setSelfStatus(_ status: UserStatus) -> Bool
```
## Return Value
是否成功(C 層 ACK;目前永遠 true,但不代表對端會收到)
## Discussion
同 `setSelfName` 的廣播與 delegate 行為,對象是 userStatus。
⚠️ **目前對端不會收到事件**:revuekit-c 0.11.x 在接收端故意不註冊 `tox_callback_friend_status`,本端 delegate 會被通知(local echo)但 對端永遠收不到 `friend_status` 事件。 追蹤中:https://git.ihh.tw/revue-kit/issue/-/issues/57 修復前若需要廣播 online/away/busy,請改走 `PresenceManager` (事件為 `friend_presence`)。
---
# setSelfStatusMessage(_:)
[RevueSDK](../../index.md) › [Revue](../index.md) › setSelfStatusMessage(_:)
*Instance Method*
設定自身狀態訊息(個人簽名)
```swift
@discardableResult @MainActor func setSelfStatusMessage(_ message: String) -> Bool
```
## Return Value
是否成功(C 層 ACK)
## Discussion
同 `setSelfName` 的廣播與 delegate 行為,對象是 statusMessage。
---
# shared
[RevueSDK](../../index.md) › [Revue](../index.md) › shared
*Type Property*
```swift
nonisolated static let shared: Revue
```
---
# startPresence(_:)
[RevueSDK](../../index.md) › [Revue](../index.md) › startPresence(_:)
*Instance Method*
Start presence, or replace the running connection with new settings.
```swift
@MainActor func startPresence(_ options: MqttOptions? = nil) async throws
```
## Parameters
- options
The settings to connect with. `nil` uses the built-in service. `deferStart` is ignored here.
## Discussion
For an App that initialized with `MqttOptions.deferred`: call this once the user has signed in. Call it again with another account to switch: the old connection closes first, so the delegate sees `.notInitialized`, then `.connected`. Calls run in the order they were made.
Returning without an error only means the request was sent, not that presence is connected. The result arrives through `RevueDelegate.revue(_:didUpdatePresenceConnectionState:)`.
`MqttOptions.deferred` applies to one `initialize(_:)`, so call this again after `destroy(_:)`.
> **Throws**
>
> `RevuePresenceError`. `.invalidConfiguration` leaves the running connection untouched.
---
# stopPresence()
[RevueSDK](../../index.md) › [Revue](../index.md) › stopPresence()
*Instance Method*
Stop presence, for example when the user signs out. Calls and friend connections are not affected.
```swift
@MainActor func stopPresence() async
```
## Discussion
The delegate gets `.notInitialized` once through `RevueDelegate.revue(_:didUpdatePresenceConnectionState:)`. Call `startPresence(_:)` to connect again. Returns right away when the SDK is not initialized or is being destroyed.
Known issue: while a friend connection is still up, friends can see this user go `.offline` and then return to `.online`, and stay there. Until this is fixed, call `destroy()` if friends must see the user as offline.
---
# RevueDelegate
[RevueSDK](../index.md) › RevueDelegate
*Protocol*
朋友層事件代理 — 監聽好友連線、訊息、Presence 等事件
```swift
protocol RevueDelegate : AnyObject
```
## Overview
使用方式:
```swift
Revue.shared.delegates.add(delegate: self)
func revue(_ revue: Revue, friend friendId: String, didUpdatePaths paths: FriendConnectionPaths) {
updateUI(friendId, isOnline: paths.isOnline)
}
```
所有方法都有預設空實作,可選擇性實作需要的事件。 所有 delegate 方法在 Main Thread 呼叫。
## Topics
### Instance Methods
- [`func revue(Revue, didReceiveFrameCryptoError: FrameCryptoError)`](revue%28_:didreceiveframecryptoerror:%29/index.md)
Frame crypto 加解密錯誤(v0.14.0,C 層 type 102)
- [`func revue(Revue, didReceiveFriendRequest: String, message: String)`](revue%28_:didreceivefriendrequest:message:%29/index.md)
收到好友請求
- [`func revue(Revue, didUpdateConnectionState: SelfConnectionStatus)`](revue%28_:didupdateconnectionstate:%29/index.md)
自身 DHT 連線狀態變更(offline/tcp/udp)
- [`func revue(Revue, didUpdatePresenceConnectionState: PresenceConnectionState)`](revue%28_:didupdatepresenceconnectionstate:%29/index.md)
自身 Presence 服務(MQTT broker)連線狀態變更
- [`func revue(Revue, friend: String, didEndInvitation: InvitationEndReason)`](revue%28_:friend:didendinvitation:%29/index.md)
雙邊共用:邀請結束(未成功進入通話)
- [`func revue(Revue, friend: String, didReceiveInvitation: String?)`](revue%28_:friend:didreceiveinvitation:%29/index.md)
接聽方視角:收到對方邀請
- [`func revue(Revue, friend: String, didReceiveMessage: String)`](revue%28_:friend:didreceivemessage:%29/index.md)
收到好友訊息(非 Room 內的一般訊息)
- [`func revue(Revue, friend: String, didSendInvitation: String?)`](revue%28_:friend:didsendinvitation:%29/index.md)
撥打方視角:我已送出邀請(對方在響鈴中)
- [`func revue(Revue, friend: String, didUpdateName: String)`](revue%28_:friend:didupdatename:%29/index.md)
好友改名
- [`func revue(Revue, friend: String, didUpdatePaths: FriendConnectionPaths)`](revue%28_:friend:didupdatepaths:%29/index.md)
好友連線路徑變更(v0.13.0-rc1:UDP 直連 / UDP 中繼 / TCP 中繼三路同時 bitmap)
- [`func revue(Revue, friend: String, didUpdatePresenceStatus: PresenceStatus)`](revue%28_:friend:didupdatepresencestatus:%29/index.md)
好友 Presence 原始狀態(offline/online/away/busy)
- [`func revue(Revue, friend: String, didUpdateStatus: UserStatus)`](revue%28_:friend:didupdatestatus:%29/index.md)
好友主動設定狀態(UserStatus: none/away/busy)
- [`func revue(Revue, friend: String, didUpdateStatusMessage: String)`](revue%28_:friend:didupdatestatusmessage:%29/index.md)
好友狀態訊息變更
---
# revue(_:didReceiveFrameCryptoError:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:didReceiveFrameCryptoError:)
*Instance Method*
Frame crypto 加解密錯誤(v0.14.0,C 層 type 102)
```swift
func revue(_ revue: Revue, didReceiveFrameCryptoError error: FrameCryptoError)
```
## Discussion
runtime 加解密失敗(callback 回非 0 / 密文超上限)→ SDK drop 該幀、不中斷通話,並發此通知。 串接端據此觀察錯誤率、決定是否提示使用者或掛斷。持續失敗=持續黑畫面/無聲。 `error` 以 `switch` 拆出方向/媒體/錯誤碼(見 `FrameCryptoError`)。
---
# revue(_:didReceiveFrameCryptoError:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:didReceiveFrameCryptoError:)
*Instance Method*
```swift
func revue(_ revue: Revue, didReceiveFrameCryptoError error: FrameCryptoError)
```
---
# revue(_:didReceiveFriendRequest:message:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:didReceiveFriendRequest:message:)
*Instance Method*
收到好友請求
```swift
func revue(_ revue: Revue, didReceiveFriendRequest friendId: String, message: String)
```
---
# revue(_:didReceiveFriendRequest:message:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:didReceiveFriendRequest:message:)
*Instance Method*
```swift
func revue(_ revue: Revue, didReceiveFriendRequest friendId: String, message: String)
```
---
# revue(_:didUpdateConnectionState:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:didUpdateConnectionState:)
*Instance Method*
自身 DHT 連線狀態變更(offline/tcp/udp)
```swift
func revue(_ revue: Revue, didUpdateConnectionState status: SelfConnectionStatus)
```
---
# revue(_:didUpdateConnectionState:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:didUpdateConnectionState:)
*Instance Method*
```swift
func revue(_ revue: Revue, didUpdateConnectionState status: SelfConnectionStatus)
```
---
# revue(_:didUpdatePresenceConnectionState:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:didUpdatePresenceConnectionState:)
*Instance Method*
自身 Presence 服務(MQTT broker)連線狀態變更
```swift
func revue(_ revue: Revue, didUpdatePresenceConnectionState state: PresenceConnectionState)
```
## Discussion
與 `didUpdateConnectionState`(DHT 連線)獨立。失去 broker 連線時, 所有 `didUpdatePresenceStatus` 資料變得不可信;重連後 SDK 會自動補發 friendPresence 事件。
應用情境:在 UI 顯示「Presence 服務不穩定」警告、暫停依賴 Presence 的功能。
---
# revue(_:didUpdatePresenceConnectionState:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:didUpdatePresenceConnectionState:)
*Instance Method*
```swift
func revue(_ revue: Revue, didUpdatePresenceConnectionState state: PresenceConnectionState)
```
---
# revue(_:friend:didEndInvitation:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didEndInvitation:)
*Instance Method*
雙邊共用:邀請結束(未成功進入通話)
```swift
func revue(_ revue: Revue, friend friendId: String, didEndInvitation reason: InvitationEndReason)
```
## Discussion
reason 視角區分:
- 撥打方:`.cancelled`(自己 cancel) / `.timeout`(沒人接/對方不可達) / `.finished`(對方結束)
- 接聽方:`.cancelled`(對方 cancel) / `.rejected`(自己 reject) / `.timeout` / `.finished`
App 由前面收到 `didSendInvitation` 或 `didReceiveInvitation` 推斷自己角色。
⚠️ `.rejected` 只在**自己**按拒接時產生 —— 撥打方收不到此值, 對方拒接時撥打方收到的是 `.finished`(見 `InvitationEndReason` 說明)。
---
# revue(_:friend:didEndInvitation:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didEndInvitation:)
*Instance Method*
```swift
func revue(_ revue: Revue, friend friendId: String, didEndInvitation reason: InvitationEndReason)
```
---
# revue(_:friend:didReceiveInvitation:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didReceiveInvitation:)
*Instance Method*
接聽方視角:收到對方邀請
```swift
func revue(_ revue: Revue, friend friendId: String, didReceiveInvitation message: String?)
```
## Parameters
- message
對方撥號時帶的 free-form opaque string(SDK 不解析、上層自訂協定)
## Discussion
由 ToxCore callIncoming 事件觸發。此時 `Room.remoteParticipants` 尚不含對方 (依設計合約「邀請階段沒有 RemoteParticipant」)。
---
# revue(_:friend:didReceiveInvitation:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didReceiveInvitation:)
*Instance Method*
```swift
func revue(_ revue: Revue, friend friendId: String, didReceiveInvitation message: String?)
```
---
# revue(_:friend:didReceiveMessage:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didReceiveMessage:)
*Instance Method*
收到好友訊息(非 Room 內的一般訊息)
```swift
func revue(_ revue: Revue, friend friendId: String, didReceiveMessage message: String)
```
---
# revue(_:friend:didReceiveMessage:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didReceiveMessage:)
*Instance Method*
```swift
func revue(_ revue: Revue, friend friendId: String, didReceiveMessage message: String)
```
---
# revue(_:friend:didSendInvitation:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didSendInvitation:)
*Instance Method*
撥打方視角:我已送出邀請(對方在響鈴中)
```swift
func revue(_ revue: Revue, friend friendId: String, didSendInvitation message: String?)
```
## Parameters
- message
撥號時帶的 free-form opaque string(SDK 不解析、上層自訂協定)
## Discussion
由 `FriendManager.sendInvitation(friendId:message:)` 觸發。
---
# revue(_:friend:didSendInvitation:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didSendInvitation:)
*Instance Method*
```swift
func revue(_ revue: Revue, friend friendId: String, didSendInvitation message: String?)
```
---
# revue(_:friend:didUpdateName:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didUpdateName:)
*Instance Method*
好友改名
```swift
func revue(_ revue: Revue, friend friendId: String, didUpdateName name: String)
```
---
# revue(_:friend:didUpdateName:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didUpdateName:)
*Instance Method*
```swift
func revue(_ revue: Revue, friend friendId: String, didUpdateName name: String)
```
---
# revue(_:friend:didUpdatePaths:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didUpdatePaths:)
*Instance Method*
好友連線路徑變更(v0.13.0-rc1:UDP 直連 / UDP 中繼 / TCP 中繼三路同時 bitmap)
```swift
func revue(_ revue: Revue, friend friendId: String, didUpdatePaths paths: FriendConnectionPaths)
```
---
# revue(_:friend:didUpdatePaths:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didUpdatePaths:)
*Instance Method*
```swift
func revue(_ revue: Revue, friend friendId: String, didUpdatePaths paths: FriendConnectionPaths)
```
---
# revue(_:friend:didUpdatePresenceStatus:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didUpdatePresenceStatus:)
*Instance Method*
好友 Presence 原始狀態(offline/online/away/busy)
```swift
func revue(_ revue: Revue, friend friendId: String, didUpdatePresenceStatus status: PresenceStatus)
```
## Discussion
MQTT broker 看到的對方狀態,獨立於 toxcore P2P 連線狀態。 與 `didUpdatePaths`(連線路徑 bitmap)和 `didUpdateStatus`(toxcore P2P 用戶狀態)區別開來。
---
# revue(_:friend:didUpdatePresenceStatus:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didUpdatePresenceStatus:)
*Instance Method*
```swift
func revue(_ revue: Revue, friend friendId: String, didUpdatePresenceStatus status: PresenceStatus)
```
---
# revue(_:friend:didUpdateStatus:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didUpdateStatus:)
*Instance Method*
好友主動設定狀態(UserStatus: none/away/busy)
```swift
func revue(_ revue: Revue, friend friendId: String, didUpdateStatus status: UserStatus)
```
---
# revue(_:friend:didUpdateStatus:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didUpdateStatus:)
*Instance Method*
```swift
func revue(_ revue: Revue, friend friendId: String, didUpdateStatus status: UserStatus)
```
---
# revue(_:friend:didUpdateStatusMessage:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didUpdateStatusMessage:)
*Instance Method*
好友狀態訊息變更
```swift
func revue(_ revue: Revue, friend friendId: String, didUpdateStatusMessage message: String)
```
---
# revue(_:friend:didUpdateStatusMessage:)
[RevueSDK](../../index.md) › [RevueDelegate](../index.md) › revue(_:friend:didUpdateStatusMessage:)
*Instance Method*
```swift
func revue(_ revue: Revue, friend friendId: String, didUpdateStatusMessage message: String)
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › Equatable Implementations
*Article*
---
# Error Implementations
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › Error Implementations
*Article*
---
# errorDescription
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › errorDescription
*Instance Property*
```swift
var errorDescription: String? { get }
```
---
# failureReason
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › failureReason
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var failureReason: String? { get }
```
---
# RevueInitError.gracePeriodTooShort
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › RevueInitError.gracePeriodTooShort
*Case*
Switch A/B 寬限期設了非 0 但小於下限 15000 毫秒 (C -27 `REVUEKIT_ERR_GRACE_PERIOD_TOO_SHORT`,@since 0.15.0)
```swift
case gracePeriodTooShort
```
---
# helpAnchor
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › helpAnchor
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var helpAnchor: String? { get }
```
---
# RevueInitError
[RevueSDK](../index.md) › RevueInitError
*Enumeration*
SDK 初始化錯誤(`Revue.initialize` throw)
```swift
enum RevueInitError
```
## Overview
v0.14.0 起 `revuekit_init` 為 fail-closed 授權:沒有有效憑證即初始化失敗。 license 四種原因(C 錯誤碼 -22 ~ -25)完整區分,讓上層可對「憑證過期」等分別提示。
## Topics
### Enumeration Cases
- [`case gracePeriodTooShort`](graceperiodtooshort/index.md)
Switch A/B 寬限期設了非 0 但小於下限 15000 毫秒 (C -27 `REVUEKIT_ERR_GRACE_PERIOD_TOO_SHORT`,@since 0.15.0)
- [`case initFailed(code: Int32)`](initfailed%28code:%29/index.md)
其他初始化失敗,帶原始 C 錯誤碼供 debug(-5/-6/其他)
- [`case invalidConfiguration`](invalidconfiguration/index.md)
初始化參數錯誤(C -1 null pointer / -2 invalid argument)
- [`case invalidTransportConfig`](invalidtransportconfig/index.md)
`NetworkOptions` 的 transport flag 組合自相矛盾 (C -32 `REVUEKIT_ERR_INVALID_TRANSPORT_CONFIG`,@since core 0.18.0)
- [`case licenseExpired`](licenseexpired/index.md)
憑證合法但已過期(C -25 `REVUEKIT_ERR_LICENSE_EXPIRED`)
- [`case licenseInvalid`](licenseinvalid/index.md)
憑證簽章不符(內容被竄改,或非本 SDK 嵌入公鑰所簽)(C -24 `REVUEKIT_ERR_LICENSE_INVALID`)
- [`case licenseMalformed`](licensemalformed/index.md)
憑證結構/TLV 錯誤、或檔案讀不到/沒打包(C -23 `REVUEKIT_ERR_LICENSE_MALFORMED`)
- [`case licenseMissing`](licensemissing/index.md)
未提供授權憑證(C -22 `REVUEKIT_ERR_LICENSE_MISSING`)
- [`case waitingForDestroy`](waitingfordestroy/index.md)
前一次 `destroy()` 尚未完成,C instance 仍存在。
### Instance Properties
- [`var errorDescription: String?`](errordescription/index.md)
---
# RevueInitError.initFailed(code:)
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › RevueInitError.initFailed(code:)
*Case*
其他初始化失敗,帶原始 C 錯誤碼供 debug(-5/-6/其他)
```swift
case initFailed(code: Int32)
```
---
# RevueInitError.invalidConfiguration
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › RevueInitError.invalidConfiguration
*Case*
初始化參數錯誤(C -1 null pointer / -2 invalid argument)
```swift
case invalidConfiguration
```
---
# RevueInitError.invalidTransportConfig
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › RevueInitError.invalidTransportConfig
*Case*
`NetworkOptions` 的 transport flag 組合自相矛盾 (C -32 `REVUEKIT_ERR_INVALID_TRANSPORT_CONFIG`,@since core 0.18.0)
```swift
case invalidTransportConfig
```
## Discussion
C 層拒絕建立 instance、不做部分初始化。兩種成因(C 層**不區分**、同回 -32):
1. `enableUdpDirect` / `enableUdpRelay` / `enableTcpRelay` 三者全 `false` —— 既無 UDP 也無 TCP transport,self DHT 必定永停 OFFLINE。
2. `switchAEnabled == true` 但兩個 relay flag 皆 `false` —— Switch A 的 `ROUTING_RELAY` 態不可達、狀態機沉默失能。
三個 flag 為 init-time 唯讀,修正方式是改 `NetworkOptions` 後重新 `initialize`: 成因 1 至少啟用一種連線方式;成因 2 改為啟用至少一條 relay,或關閉 Switch A。
---
# RevueInitError.licenseExpired
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › RevueInitError.licenseExpired
*Case*
憑證合法但已過期(C -25 `REVUEKIT_ERR_LICENSE_EXPIRED`)
```swift
case licenseExpired
```
---
# RevueInitError.licenseInvalid
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › RevueInitError.licenseInvalid
*Case*
憑證簽章不符(內容被竄改,或非本 SDK 嵌入公鑰所簽)(C -24 `REVUEKIT_ERR_LICENSE_INVALID`)
```swift
case licenseInvalid
```
---
# RevueInitError.licenseMalformed
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › RevueInitError.licenseMalformed
*Case*
憑證結構/TLV 錯誤、或檔案讀不到/沒打包(C -23 `REVUEKIT_ERR_LICENSE_MALFORMED`)
```swift
case licenseMalformed
```
---
# RevueInitError.licenseMissing
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › RevueInitError.licenseMissing
*Case*
未提供授權憑證(C -22 `REVUEKIT_ERR_LICENSE_MISSING`)
```swift
case licenseMissing
```
---
# localizedDescription
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › localizedDescription
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var localizedDescription: String { get }
```
---
# LocalizedError Implementations
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › LocalizedError Implementations
*Article*
---
# recoverySuggestion
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › recoverySuggestion
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var recoverySuggestion: String? { get }
```
---
# RevueInitError.waitingForDestroy
[RevueSDK](../../index.md) › [RevueInitError](../index.md) › RevueInitError.waitingForDestroy
*Case*
前一次 `destroy()` 尚未完成,C instance 仍存在。
```swift
case waitingForDestroy
```
## Discussion
`destroy()` 為 async(內部等 core loop 收斂,core 阻塞時可能數十秒), 其等待期間呼叫 `initialize()` 會落在此。等 destroy 返回後重試即可。
與 `invalidConfiguration` 區分:兩者在 C 層同為 -2,但成因與處置完全不同 (改參數 vs 等待重試),故由 `initialize()` 依 instance 狀態自行判定、 不經過 `from(code:)` 的純碼對映。
---
# file
[RevueSDK](../../index.md) › [RevueLogEntry](../index.md) › file
*Instance Property*
來源檔名(debug 用,Swift 端 helper 用 #file 自動填、C callback 給的也是絕對路徑)
```swift
let file: String?
```
---
# function
[RevueSDK](../../index.md) › [RevueLogEntry](../index.md) › function
*Instance Property*
函式名
```swift
let function: String?
```
---
# RevueLogEntry
[RevueSDK](../index.md) › RevueLogEntry
*Structure*
SDK log 條目(C 核心層 log 透過 callback 進來、Swift 端組成此型別推給訂閱者)
```swift
nonisolated struct RevueLogEntry
```
## Overview
對外暴露透過 `Revue.shared.logPublisher: AnyPublisher`。
## Topics
### Initializers
- [`init(level: RevueLogLevel, source: String, message: String, timestamp: Date, file: String?, line: UInt32?, function: String?)`](init%28level:source:message:timestamp:file:line:function:%29/index.md)
### Instance Properties
- [`let file: String?`](file/index.md)
來源檔名(debug 用,Swift 端 helper 用 #file 自動填、C callback 給的也是絕對路徑)
- [`let function: String?`](function/index.md)
函式名
- [`let level: RevueLogLevel`](level/index.md)
log 級別
- [`let line: UInt32?`](line/index.md)
行號
- [`let message: String`](message/index.md)
log 訊息正文
- [`let source: String`](source/index.md)
模組來源(如 “revuekit” / “toxcore” / “mqtt” / “presence” / “NetworkMonitor”)
- [`let timestamp: Date`](timestamp/index.md)
log 產生時間(callback 收到瞬間 / Swift 端建立瞬間)
---
# init(level:source:message:timestamp:file:line:function:)
[RevueSDK](../../index.md) › [RevueLogEntry](../index.md) › init(level:source:message:timestamp:file:line:function:)
*Initializer*
```swift
init(level: RevueLogLevel, source: String, message: String, timestamp: Date = Date(), file: String? = nil, line: UInt32? = nil, function: String? = nil)
```
---
# level
[RevueSDK](../../index.md) › [RevueLogEntry](../index.md) › level
*Instance Property*
log 級別
```swift
let level: RevueLogLevel
```
---
# line
[RevueSDK](../../index.md) › [RevueLogEntry](../index.md) › line
*Instance Property*
行號
```swift
let line: UInt32?
```
---
# message
[RevueSDK](../../index.md) › [RevueLogEntry](../index.md) › message
*Instance Property*
log 訊息正文
```swift
let message: String
```
---
# source
[RevueSDK](../../index.md) › [RevueLogEntry](../index.md) › source
*Instance Property*
模組來源(如 “revuekit” / “toxcore” / “mqtt” / “presence” / “NetworkMonitor”)
```swift
let source: String
```
---
# timestamp
[RevueSDK](../../index.md) › [RevueLogEntry](../index.md) › timestamp
*Instance Property*
log 產生時間(callback 收到瞬間 / Swift 端建立瞬間)
```swift
let timestamp: Date
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# ...(_:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › ...(_:)
*Operator*
```swift
static func ... (minimum: Self) -> PartialRangeFrom
```
---
# ...(_:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › ...(_:)
*Operator*
```swift
static func ... (maximum: Self) -> PartialRangeThrough
```
---
# ...(_:_:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › ...(_:_:)
*Operator*
```swift
static func ... (minimum: Self, maximum: Self) -> ClosedRange
```
---
# ..<(_:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › ..<(_:)
*Operator*
```swift
static func ..< (maximum: Self) -> PartialRangeUpTo
```
---
# ..<(_:_:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › ..<(_:_:)
*Operator*
```swift
static func ..< (minimum: Self, maximum: Self) -> Range
```
---
# <(_:_:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › <(_:_:)
*Operator*
```swift
static func < (lhs: RevueLogLevel, rhs: RevueLogLevel) -> Bool
```
---
# >(_:_:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › >(_:_:)
*Operator*
```swift
static func > (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# <=(_:_:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › <=(_:_:)
*Operator*
```swift
static func <= (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# >=(_:_:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › >=(_:_:)
*Operator*
```swift
static func >= (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Comparable Implementations
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › Comparable Implementations
*Article*
---
# RevueLogLevel.debug
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › RevueLogLevel.debug
*Case*
除錯訊息
```swift
case debug
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › Equatable Implementations
*Article*
---
# RevueLogLevel.error
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › RevueLogLevel.error
*Case*
錯誤
```swift
case error
```
---
# hash(into:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# RevueLogLevel
[RevueSDK](../index.md) › RevueLogLevel
*Enumeration*
log 級別(對應 C 層 `RevueKitLogLevel`,rawValue 直接對齊 Int32 ABI)
```swift
nonisolated enum RevueLogLevel
```
## Topics
### Operators
- [`static func < (RevueLogLevel, RevueLogLevel) -> Bool`](_%28_:_:%29/index.md)
### Enumeration Cases
- [`case debug`](debug/index.md)
除錯訊息
- [`case error`](error/index.md)
錯誤
- [`case info`](info/index.md)
一般資訊
- [`case trace`](trace/index.md)
詳細追蹤(網路封包等)
- [`case warning`](warning/index.md)
警告
### Initializers
- [`init?(rawValue: Int32)`](init%28rawvalue:%29/index.md)
### Instance Properties
- [`var name: String`](name/index.md)
大寫名稱(如 “TRACE” / “WARNING”),方便格式化
---
# RevueLogLevel.info
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › RevueLogLevel.info
*Case*
一般資訊
```swift
case info
```
---
# init(rawValue:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int32)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# name
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › name
*Instance Property*
大寫名稱(如 “TRACE” / “WARNING”),方便格式化
```swift
var name: String { get }
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › RawRepresentable Implementations
*Article*
---
# RevueLogLevel.trace
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › RevueLogLevel.trace
*Case*
詳細追蹤(網路封包等)
```swift
case trace
```
---
# RevueLogLevel.warning
[RevueSDK](../../index.md) › [RevueLogLevel](../index.md) › RevueLogLevel.warning
*Case*
警告
```swift
case warning
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › Equatable Implementations
*Article*
---
# Error Implementations
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › Error Implementations
*Article*
---
# errorDescription
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › errorDescription
*Instance Property*
```swift
var errorDescription: String? { get }
```
---
# failureReason
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › failureReason
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var failureReason: String? { get }
```
---
# from(code:)
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › from(code:)
*Type Method*
Maps a `revuekit_presence_start` return code. Call it only when the code is not 0.
```swift
static func from(code: Int32) -> RevuePresenceError
```
## Discussion
A pure function that does not touch the C singleton.
---
# helpAnchor
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › helpAnchor
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var helpAnchor: String? { get }
```
---
# RevuePresenceError
[RevueSDK](../index.md) › RevuePresenceError
*Enumeration*
Why `Revue.startPresence(_:)` failed.
```swift
enum RevuePresenceError
```
## Topics
### Enumeration Cases
- [`case invalidConfiguration`](invalidconfiguration/index.md)
`presenceQos` is outside 0…2, or the core rejected the settings, such as a broker URL with an unsupported scheme (C -2). The running connection is not affected.
- [`case notInitialized`](notinitialized/index.md)
The SDK is not initialized, or was destroyed before the call ran (C -1 / -4).
- [`case startFailed(code: Int32)`](startfailed%28code:%29/index.md)
The MQTT client could not be created or the connection request failed, leaving presence stopped. Carries the C error code for debugging.
### Instance Properties
- [`var errorDescription: String?`](errordescription/index.md)
### Type Methods
- [`static func from(code: Int32) -> RevuePresenceError`](from%28code:%29/index.md)
Maps a `revuekit_presence_start` return code. Call it only when the code is not 0.
---
# RevuePresenceError.invalidConfiguration
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › RevuePresenceError.invalidConfiguration
*Case*
`presenceQos` is outside 0…2, or the core rejected the settings, such as a broker URL with an unsupported scheme (C -2). The running connection is not affected.
```swift
case invalidConfiguration
```
---
# localizedDescription
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › localizedDescription
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var localizedDescription: String { get }
```
---
# LocalizedError Implementations
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › LocalizedError Implementations
*Article*
---
# RevuePresenceError.notInitialized
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › RevuePresenceError.notInitialized
*Case*
The SDK is not initialized, or was destroyed before the call ran (C -1 / -4).
```swift
case notInitialized
```
---
# recoverySuggestion
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › recoverySuggestion
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var recoverySuggestion: String? { get }
```
---
# RevuePresenceError.startFailed(code:)
[RevueSDK](../../index.md) › [RevuePresenceError](../index.md) › RevuePresenceError.startFailed(code:)
*Case*
The MQTT client could not be created or the connection request failed, leaving presence stopped. Carries the C error code for debugging.
```swift
case startFailed(code: Int32)
```
---
# broadcastFinished()
[RevueSDK](../../index.md) › [RevueSampleHandler](../index.md) › broadcastFinished()
*Instance Method*
- macCatalyst 13.1+
```swift
override func broadcastFinished()
```
---
# broadcastPaused()
[RevueSDK](../../index.md) › [RevueSampleHandler](../index.md) › broadcastPaused()
*Instance Method*
- macCatalyst 13.1+
```swift
override func broadcastPaused()
```
---
# broadcastResumed()
[RevueSDK](../../index.md) › [RevueSampleHandler](../index.md) › broadcastResumed()
*Instance Method*
- macCatalyst 13.1+
```swift
override func broadcastResumed()
```
---
# broadcastStarted(withSetupInfo:)
[RevueSDK](../../index.md) › [RevueSampleHandler](../index.md) › broadcastStarted(withSetupInfo:)
*Instance Method*
- macCatalyst 13.1+
```swift
override func broadcastStarted(withSetupInfo _: [String : NSObject]?)
```
---
# connectionDidClose(error:)
[RevueSDK](../../index.md) › [RevueSampleHandler](../index.md) › connectionDidClose(error:)
*Instance Method*
Override 點:Socket 連線關閉時的處理
- macCatalyst 13.1+
```swift
func connectionDidClose(error: Error?)
```
## Discussion
- 預設行為:有 error 呼叫 `finishBroadcastWithError`,無 error 呼叫 `finishBroadcastWithoutError`
- 觸發時機:
1. 主 App 端呼叫 `BroadcastManager.requestStop()`
2. Unix socket 意外斷線(主 App 端關閉、進程 crash)
3. IPC 上傳失敗
---
# enableLogging
[RevueSDK](../../index.md) › [RevueSampleHandler](../index.md) › enableLogging
*Instance Property*
是否啟用 log(預設 false)。啟用後輸出哪些等級由主 App 的 `Revue.setLogLevel` 決定, 分享中途改動也會套用;沒設定 App Group 時只輸出 warning 和 error。
- macCatalyst 13.1+
```swift
var enableLogging: Bool { get }
```
---
# RevueSampleHandler
[RevueSDK](../index.md) › RevueSampleHandler
*Class*
Extension 端 base class — 開發者繼承後只需 override `enableLogging`(可選)
- macCatalyst 13.1+
```swift
class RevueSampleHandler
```
## Overview
> **Note**
>
> Xcode「新增 Broadcast Upload Extension」template 產生的 `SampleHandler.swift` 把 `class SampleHandler: RPBroadcastSampleHandler` 改成 `class SampleHandler: RevueSampleHandler` 即可。
## Topics
### Initializers
- [`init()`](init%28%29/index.md)
### Instance Properties
- [`var enableLogging: Bool`](enablelogging/index.md)
是否啟用 log(預設 false)。啟用後輸出哪些等級由主 App 的 `Revue.setLogLevel` 決定, 分享中途改動也會套用;沒設定 App Group 時只輸出 warning 和 error。
- [`var verboseLogging: Bool`](verboselogging/index.md)
是否啟用 verbose log(預設 false)
### Instance Methods
- [`func broadcastFinished()`](broadcastfinished%28%29/index.md)
- [`func broadcastPaused()`](broadcastpaused%28%29/index.md)
- [`func broadcastResumed()`](broadcastresumed%28%29/index.md)
- [`func broadcastStarted(withSetupInfo: [String : NSObject]?)`](broadcaststarted%28withsetupinfo:%29/index.md)
- [`func connectionDidClose(error: Error?)`](connectiondidclose%28error:%29/index.md)
Override 點:Socket 連線關閉時的處理
- [`func processSampleBuffer(CMSampleBuffer, with: RPSampleBufferType)`](processsamplebuffer%28_:with:%29/index.md)
---
# init()
[RevueSDK](../../index.md) › [RevueSampleHandler](../index.md) › init()
*Initializer*
- macCatalyst 13.1+
```swift
override init()
```
---
# processSampleBuffer(_:with:)
[RevueSDK](../../index.md) › [RevueSampleHandler](../index.md) › processSampleBuffer(_:with:)
*Instance Method*
- macCatalyst 13.1+
```swift
override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with type: RPSampleBufferType)
```
---
# verboseLogging
[RevueSDK](../../index.md) › [RevueSampleHandler](../index.md) › verboseLogging
*Instance Property*
是否啟用 verbose log(預設 false)
- macCatalyst 13.1+
```swift
var verboseLogging: Bool { get }
```
---
# activeSpeakers
[RevueSDK](../../index.md) › [Room](../index.md) › activeSpeakers
*Instance Property*
正在說話的參與者列表(依音量由大到小,含本地自己)
```swift
@Published var activeSpeakers: [Participant] { get set }
```
## Discussion
由 `Room+ActiveSpeakers` 的 400ms 輪詢更新。`internal(set)` 而非 `private(set)`: `private(set)` 是檔案級可見性,寫入者位於 `Room+ActiveSpeakers.swift`。對外行為 不變(外部仍只能讀)。
---
# allParticipants
[RevueSDK](../../index.md) › [Room](../index.md) › allParticipants
*Instance Property*
所有參與者(含自己)
```swift
var allParticipants: [Participant] { get }
```
---
# delegates
[RevueSDK](../../index.md) › [Room](../index.md) › delegates
*Instance Property*
多播代理
```swift
let delegates: MulticastDelegate
```
---
# disconnectAll()
[RevueSDK](../../index.md) › [Room](../index.md) › disconnectAll()
*Instance Method*
斷開所有遠端參與者(Room 本身不消失)
```swift
@MainActor func disconnectAll()
```
## Discussion
對所有 `remoteParticipants` 透過 ToxAV hangUp,並 fire `participantDidDisconnect` 給每個被斷開的 participant(雙 fire ParticipantDelegate + RoomDelegate)。 `Room.shared` 是 singleton,斷開後可重新透過 `FriendManager.sendInvitation` 發起新邀請。
```swift
Room.shared.disconnectAll()
// 之後可以重新邀請
Revue.shared.friends.sendInvitation(friendId: "new-friend")
```
必須在 main thread 呼叫(`@MainActor`):清理會改 `@Published` 狀態, 事件也在 main 同步送出。
---
# Room
[RevueSDK](../index.md) › Room
*Class*
會議室 — 管理參與者、Track 發佈/訂閱的核心物件(singleton)
```swift
class Room
```
## Overview
RevueSDK 是直連架構,ToxCore 同時只支援一個群組通話 session, 且相機/麥克風只有一個,因此 Room 設計為 singleton。 localParticipant 隨 SDK 初始化就存在,不需要額外 join。
邀請與接聽在 `Revue.shared.friends`(`FriendManager`)上,結果由 `RevueDelegate` 回報:
- `sendInvitation(friendId:message:)` — 邀請好友
- `acceptInvitation(friendId:)` / `rejectInvitation(friendId:)` — 接受 / 拒絕收到的邀請
對方接通後以 `RemoteParticipant` 加入 `remoteParticipants`,並 fire `RoomDelegate.participantDidConnect`。斷線操作:
- `remoteParticipant.disconnect()` — 斷開與此人的連線
- `disconnectAll()` — 斷開所有遠端參與者(Room 本身不消失)
使用方式:
```swift
// 每次 Revue.shared.initialize() 之後註冊(destroy() 會清空 delegates)
Room.shared.delegates.add(delegate: self)
// 邀請好友;對方接受後 fire participantDidConnect
try Revue.shared.friends.sendInvitation(friendId: "alice")
// 收到邀請(在 RevueDelegate.didReceiveInvitation 中)
try Revue.shared.friends.acceptInvitation(friendId: "bob")
// 斷開特定人
Room.shared.remoteParticipants["bob"]?.disconnect()
// 斷開所有遠端參與者(Room 本身不消失)
Room.shared.disconnectAll()
```
## Topics
### Instance Properties
- [`var activeSpeakers: [Participant]`](activespeakers/index.md)
正在說話的參與者列表(依音量由大到小,含本地自己)
- [`var allParticipants: [Participant]`](allparticipants/index.md)
所有參與者(含自己)
- [`let delegates: MulticastDelegate`](delegates/index.md)
多播代理
- [`var isSpeakerMuted: Bool`](isspeakermuted/index.md)
Speaker 靜音
- [`var localParticipant: LocalParticipant!`](localparticipant/index.md)
本地參與者(自己)— 隨 SDK 初始化就存在
- [`var participantCount: Int`](participantcount/index.md)
參與者總數(含自己)
- [`var publishersCount: Int`](publisherscount/index.md)
正在發佈 Track 的參與者數量
- [`var remoteParticipants: [String : RemoteParticipant]`](remoteparticipants/index.md)
遠端參與者字典 [friendId: RemoteParticipant]
- [`var reportStatistics: Bool`](reportstatistics/index.md)
診斷時打開,執行期可切、不用重啟。
- [`var roomOutputTrack: LocalAudioTrack?`](roomoutputtrack/index.md)
所有遠端音訊的混音結果(外部開發者可透過 renderer 訂閱)
### Instance Methods
- [`func disconnectAll()`](disconnectall%28%29/index.md)
斷開所有遠端參與者(Room 本身不消失)
### Type Properties
- [`static let shared: Room`](shared/index.md)
全域唯一實例
---
# isSpeakerMuted
[RevueSDK](../../index.md) › [Room](../index.md) › isSpeakerMuted
*Instance Property*
Speaker 靜音
```swift
var isSpeakerMuted: Bool
```
## Discussion
靜音後 SDK 內部停止播放所有收到的 audioTrack,但不影響接收(錄影等功能仍可使用)。
---
# localParticipant
[RevueSDK](../../index.md) › [Room](../index.md) › localParticipant
*Instance Property*
本地參與者(自己)— 隨 SDK 初始化就存在
```swift
@Published var localParticipant: LocalParticipant! { get set }
```
---
# objectWillChange
[RevueSDK](../../index.md) › [Room](../index.md) › objectWillChange
*Instance Property*
- iOS 13.0+
- iPadOS 13.0+
- macCatalyst 13.0+
- macOS 10.15+
- tvOS 13.0+
- watchOS 6.0+
```swift
var objectWillChange: ObservableObjectPublisher { get }
```
---
# ObservableObject Implementations
[RevueSDK](../../index.md) › [Room](../index.md) › ObservableObject Implementations
*Article*
---
# participantCount
[RevueSDK](../../index.md) › [Room](../index.md) › participantCount
*Instance Property*
參與者總數(含自己)
```swift
var participantCount: Int { get }
```
---
# publishersCount
[RevueSDK](../../index.md) › [Room](../index.md) › publishersCount
*Instance Property*
正在發佈 Track 的參與者數量
```swift
var publishersCount: Int { get }
```
---
# remoteParticipants
[RevueSDK](../../index.md) › [Room](../index.md) › remoteParticipants
*Instance Property*
遠端參與者字典 [friendId: RemoteParticipant]
```swift
@Published var remoteParticipants: [String : RemoteParticipant] { get set }
```
---
# reportStatistics
[RevueSDK](../../index.md) › [Room](../index.md) › reportStatistics
*Instance Property*
診斷時打開,執行期可切、不用重啟。
```swift
var reportStatistics: Bool { get set }
```
## Discussion
- `true` → 啟動採集 timer,每秒把各 track 的 `TrackStatistics` 寫入 + fire `didUpdateStatistics`。
- `false` → 停 timer 省電,`track.statistics` 不再更新。
因 P2P singleton 無 per-room options,改用 `Room.shared` 執行期開關。
註:地基 A 原定「無 didSet 純旗標」(對齊 `isSpeakerMuted`),但為滿足地基 D 「false→停 timer 省電」的實質目標,此處加 didSet 啟停 timer。對使用者仍是「設個 bool」、無感差異。
---
# roomOutputTrack
[RevueSDK](../../index.md) › [Room](../index.md) › roomOutputTrack
*Instance Property*
所有遠端音訊的混音結果(外部開發者可透過 renderer 訂閱)
```swift
var roomOutputTrack: LocalAudioTrack? { get }
```
---
# shared
[RevueSDK](../../index.md) › [Room](../index.md) › shared
*Type Property*
全域唯一實例
```swift
static let shared: Room
```
---
# RoomDelegate
[RevueSDK](../index.md) › RoomDelegate
*Protocol*
房間事件代理 — 監聽連線、通話、參與者、Track 等事件
```swift
protocol RoomDelegate : AnyObject
```
## Overview
參與者事件兩層同步收到:同一事件會同時通知 `Room.shared.delegates`(本協定)與該參與者的 `participant.delegates`(`ParticipantDelegate` 的對應方法)。
⚠️ 會跳出提示的事件(例如收到靜音請求就彈窗、收到訊息就推通知)**只監聽其中一層**, 兩層都接會提示兩次。
使用方式:
```swift
// 每次 Revue.shared.initialize() 之後註冊:destroy() 會清空 Room.shared.delegates,
// 重新 initialize 後要再註冊一次
Room.shared.delegates.add(delegate: self)
// 參與者進房(對方接受邀請後 fire)
func room(_ room: Room, participantDidConnect participant: RemoteParticipant) {
showCallScreen(for: participant)
}
// 參與者離開房間
func room(_ room: Room, participantDidDisconnect participant: RemoteParticipant) {
// 從 participant.lastDisconnectReason 區分 .leave / .networkLost
dismissCallScreen(reason: participant.lastDisconnectReason)
}
// 連線路徑變更(v0.13.0-rc1:path bitmap)
func room(_ room: Room, participant: RemoteParticipant,
didUpdatePaths paths: FriendConnectionPaths) {
if paths.contains(.udpDirect) { print("\(participant.identity) 走 UDP 直連") }
}
// 來電由 RevueDelegate.didReceiveInvitation 處理(不在 RoomDelegate)
```
所有方法都有預設空實作,可選擇性實作需要的事件。
## Topics
### Instance Methods
- [`func room(Room, didUpdateSpeakingParticipants: [Participant])`](room%28_:didupdatespeakingparticipants:%29/index.md)
說話者列表變更(依音量由大到小,含本地自己)
- [`func room(Room, localParticipant: LocalParticipant, didPublishTrack: LocalTrackPublication)`](room%28_:localparticipant:didpublishtrack:%29/index.md)
本地 Track 已發佈
- [`func room(Room, localParticipant: LocalParticipant, didUnpublishTrack: LocalTrackPublication)`](room%28_:localparticipant:didunpublishtrack:%29/index.md)
本地 Track 已取消發佈
- [`func room(Room, participant: RemoteParticipant, didReceiveMessage: String)`](room%28_:participant:didreceivemessage:%29/index.md)
收到 Room 內參與者訊息
- [`func room(Room, participant: RemoteParticipant, didReceiveMuteRequest: Bool)`](room%28_:participant:didreceivemuterequest:%29/index.md)
收到遠端靜音請求
- [`func room(Room, participant: RemoteParticipant, didSubscribeTrack: RemoteTrackPublication)`](room%28_:participant:didsubscribetrack:%29/index.md)
已訂閱遠端 Track(收到對方的視訊/音訊)
- [`func room(Room, participant: RemoteParticipant, didUnsubscribeTrack: RemoteTrackPublication)`](room%28_:participant:didunsubscribetrack:%29/index.md)
已取消訂閱遠端 Track
- [`func room(Room, participant: Participant, didUpdateConnectionQuality: ConnectionQuality)`](room%28_:participant:didupdateconnectionquality:%29/index.md)
參與者連線品質變更(5 級:excellent / good / fair / poor / bad)
- [`func room(Room, participant: Participant, didUpdateName: String?)`](room%28_:participant:didupdatename:%29/index.md)
參與者名稱已更新(本地或遠端)
- [`func room(Room, participant: RemoteParticipant, didUpdatePaths: FriendConnectionPaths)`](room%28_:participant:didupdatepaths:%29/index.md)
參與者連線路徑變更(FriendConnectionPaths bitmap)
- [`func room(Room, participant: Participant, didUpdateStatus: UserStatus)`](room%28_:participant:didupdatestatus:%29/index.md)
參與者使用者狀態已更新(本地或遠端)
- [`func room(Room, participant: Participant, didUpdateStatusMessage: String?)`](room%28_:participant:didupdatestatusmessage:%29/index.md)
參與者狀態訊息已更新(本地或遠端);被清空時 `message` 為 nil
- [`func room(Room, participant: Participant, trackPublication: TrackPublication, didUpdateIsMuted: Bool)`](room%28_:participant:trackpublication:didupdateismuted:%29/index.md)
Track 靜音狀態變更
- [`func room(Room, participant: Participant, trackPublication: TrackPublication, didUpdateStatistics: TrackStatistics)`](room%28_:participant:trackpublication:didupdatestatistics:%29/index.md)
Track 診斷統計更新(每秒一次,僅 `reportStatistics == true` 時 fire、且有變化才 fire)。 與 `ParticipantDelegate.didUpdateStatistics` 雙 fire、與 `@Published track.statistics` 同源。
- [`func room(Room, participantDidConnect: RemoteParticipant)`](room%28_:participantdidconnect:%29/index.md)
參與者加入房間(對方接受邀請後 fire)
- [`func room(Room, participantDidDisconnect: RemoteParticipant)`](room%28_:participantdiddisconnect:%29/index.md)
參與者離開房間
---
# room(_:didUpdateSpeakingParticipants:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:didUpdateSpeakingParticipants:)
*Instance Method*
說話者列表變更(依音量由大到小,含本地自己)
```swift
func room(_ room: Room, didUpdateSpeakingParticipants speakers: [Participant])
```
## Discussion
由 Room 的語音聚焦輪詢每 400ms 判定,**僅在名單或音量量化值有變化時** fire。 傳入的是完整名單(非增量);某人停止說話即從名單消失。
---
# room(_:didUpdateSpeakingParticipants:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:didUpdateSpeakingParticipants:)
*Instance Method*
```swift
func room(_ room: Room, didUpdateSpeakingParticipants speakers: [Participant])
```
---
# room(_:localParticipant:didPublishTrack:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:localParticipant:didPublishTrack:)
*Instance Method*
本地 Track 已發佈
```swift
func room(_ room: Room, localParticipant: LocalParticipant, didPublishTrack publication: LocalTrackPublication)
```
---
# room(_:localParticipant:didPublishTrack:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:localParticipant:didPublishTrack:)
*Instance Method*
```swift
func room(_ room: Room, localParticipant: LocalParticipant, didPublishTrack publication: LocalTrackPublication)
```
---
# room(_:localParticipant:didUnpublishTrack:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:localParticipant:didUnpublishTrack:)
*Instance Method*
本地 Track 已取消發佈
```swift
func room(_ room: Room, localParticipant: LocalParticipant, didUnpublishTrack publication: LocalTrackPublication)
```
---
# room(_:localParticipant:didUnpublishTrack:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:localParticipant:didUnpublishTrack:)
*Instance Method*
```swift
func room(_ room: Room, localParticipant: LocalParticipant, didUnpublishTrack publication: LocalTrackPublication)
```
---
# room(_:participant:didReceiveMessage:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didReceiveMessage:)
*Instance Method*
收到 Room 內參與者訊息
```swift
func room(_ room: Room, participant: RemoteParticipant, didReceiveMessage message: String)
```
## Discussion
由 `LocalParticipant.publish(message:to:)` 送達。觸發條件:
1. 對方透過 Room 層 API 送訊息
2. envelope type == “msg” 且格式合法
3. delta(now - ts)<= 10 秒
4. 對方 friendId 仍在 Room.remoteParticipants 內
收不到訊息的常見原因:
- 對方用 `FriendManager.sendMessage` 送 → 會觸發 `RevueDelegate.didReceiveMessage`
- 訊息延遲超過 10 秒 → SDK 靜默丟棄
- 對方已不在 Room → SDK 靜默丟棄
---
# room(_:participant:didReceiveMessage:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didReceiveMessage:)
*Instance Method*
```swift
func room(_ room: Room, participant: RemoteParticipant, didReceiveMessage message: String)
```
---
# room(_:participant:didReceiveMuteRequest:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didReceiveMuteRequest:)
*Instance Method*
收到遠端靜音請求
```swift
func room(_ room: Room, participant: RemoteParticipant, didReceiveMuteRequest isMuted: Bool)
```
## Discussion
對方呼叫 `remoteParticipant.requestMute(true)` 時觸發。 SDK 只負責傳遞通知(能力層),是否配合由上層業務邏輯決定(授權層)。
```swift
func room(_ room: Room, participant: RemoteParticipant, didReceiveMuteRequest isMuted: Bool) {
if shouldComply {
Room.shared.localParticipant.setMicrophone(enabled: !isMuted)
}
}
```
---
# room(_:participant:didReceiveMuteRequest:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didReceiveMuteRequest:)
*Instance Method*
```swift
func room(_ room: Room, participant: RemoteParticipant, didReceiveMuteRequest isMuted: Bool)
```
---
# room(_:participant:didSubscribeTrack:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didSubscribeTrack:)
*Instance Method*
已訂閱遠端 Track(收到對方的視訊/音訊)
```swift
func room(_ room: Room, participant: RemoteParticipant, didSubscribeTrack publication: RemoteTrackPublication)
```
---
# room(_:participant:didSubscribeTrack:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didSubscribeTrack:)
*Instance Method*
```swift
func room(_ room: Room, participant: RemoteParticipant, didSubscribeTrack publication: RemoteTrackPublication)
```
---
# room(_:participant:didUnsubscribeTrack:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didUnsubscribeTrack:)
*Instance Method*
已取消訂閱遠端 Track
```swift
func room(_ room: Room, participant: RemoteParticipant, didUnsubscribeTrack publication: RemoteTrackPublication)
```
---
# room(_:participant:didUnsubscribeTrack:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didUnsubscribeTrack:)
*Instance Method*
```swift
func room(_ room: Room, participant: RemoteParticipant, didUnsubscribeTrack publication: RemoteTrackPublication)
```
---
# room(_:participant:didUpdateConnectionQuality:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didUpdateConnectionQuality:)
*Instance Method*
參與者連線品質變更(5 級:excellent / good / fair / poor / bad)
```swift
func room(_ room: Room, participant: Participant, didUpdateConnectionQuality quality: ConnectionQuality)
```
## Discussion
scorer.go:`score = 100 − 延遲扣分 − 丟包扣分` → 切級 + 降快升慢), **僅在等級改變時 fire**,同級不重複通知。
與 `didUpdatePaths` 的區別:
- `didUpdatePaths` — 路徑**通不通**(bitmap,斷線判定用)
- 本 callback — 通著時的**好不好**(RTT / jitter / 丟包率綜合評分)
通話中對方網路劣化(但未斷線)即由此得知。
---
# room(_:participant:didUpdateConnectionQuality:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didUpdateConnectionQuality:)
*Instance Method*
```swift
func room(_ room: Room, participant: Participant, didUpdateConnectionQuality quality: ConnectionQuality)
```
---
# room(_:participant:didUpdateName:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didUpdateName:)
*Instance Method*
參與者名稱已更新(本地或遠端)
```swift
func room(_ room: Room, participant: Participant, didUpdateName name: String?)
```
## Discussion
- 本地:`LocalParticipant.set(name:)` 或 `Revue.shared.setSelfName(_:)` 呼叫成功後
- 遠端:收到 `friend_name` 事件時,若 friendId 在 `Room.remoteParticipants` 內
第二參數是 `Participant` 基類,Local / Remote 兩種情境共用同一段處理邏輯。 名字被清空時 `name` 為 nil。
---
# room(_:participant:didUpdateName:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didUpdateName:)
*Instance Method*
```swift
func room(_ room: Room, participant: Participant, didUpdateName name: String?)
```
---
# room(_:participant:didUpdatePaths:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didUpdatePaths:)
*Instance Method*
參與者連線路徑變更(FriendConnectionPaths bitmap)
```swift
func room(_ room: Room, participant: RemoteParticipant, didUpdatePaths paths: FriendConnectionPaths)
```
## Discussion
對齊 `RevueDelegate.didUpdatePaths`(雙 fire)。**含空 bitmap**(v0.17.0 起)。
## ⚠️ 收到空 bitmap 時不可移除 participant
revuekit-c 0.17.0 契約(`docs/contract/av_call.md` @invariant 3): **bitmap 歸零不蘊含 session 已結束**。空窗期間 C 層持續嘗試重連 —— 網路恢復則 paths 自動回報有連線、通話自動恢復。
上層應:
- ✅ 顯示「連線中斷」之類的暫時狀態
- ✅ **保持掛斷入口可用**(`participant.disconnect()` 無連線前置條件,隨時可呼叫)
- ❌ **不得**移除 participant / 結束通話 UI —— 那會讓使用者失去掛斷入口, 只能重啟 App(違反契約約定 2、3)
真正的結束一律由 `participantDidDisconnect` 宣告。
```swift
func room(_ room: Room, participant: RemoteParticipant,
didUpdatePaths paths: FriendConnectionPaths) {
if paths.isEmpty {
showReconnectingBadge(for: participant) // 保留 participant
} else if paths.contains(.udpDirect) {
/* 走 UDP 直連 */
}
}
```
---
# room(_:participant:didUpdatePaths:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didUpdatePaths:)
*Instance Method*
```swift
func room(_ room: Room, participant: RemoteParticipant, didUpdatePaths paths: FriendConnectionPaths)
```
---
# room(_:participant:didUpdateStatus:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didUpdateStatus:)
*Instance Method*
參與者使用者狀態已更新(本地或遠端)
```swift
func room(_ room: Room, participant: Participant, didUpdateStatus status: UserStatus)
```
---
# room(_:participant:didUpdateStatus:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didUpdateStatus:)
*Instance Method*
```swift
func room(_ room: Room, participant: Participant, didUpdateStatus status: UserStatus)
```
---
# room(_:participant:didUpdateStatusMessage:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didUpdateStatusMessage:)
*Instance Method*
參與者狀態訊息已更新(本地或遠端);被清空時 `message` 為 nil
```swift
func room(_ room: Room, participant: Participant, didUpdateStatusMessage message: String?)
```
---
# room(_:participant:didUpdateStatusMessage:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:didUpdateStatusMessage:)
*Instance Method*
```swift
func room(_ room: Room, participant: Participant, didUpdateStatusMessage message: String?)
```
---
# room(_:participant:trackPublication:didUpdateIsMuted:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:trackPublication:didUpdateIsMuted:)
*Instance Method*
Track 靜音狀態變更
```swift
func room(_ room: Room, participant: Participant, trackPublication: TrackPublication, didUpdateIsMuted isMuted: Bool)
```
---
# room(_:participant:trackPublication:didUpdateIsMuted:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:trackPublication:didUpdateIsMuted:)
*Instance Method*
```swift
func room(_ room: Room, participant: Participant, trackPublication: TrackPublication, didUpdateIsMuted isMuted: Bool)
```
---
# room(_:participant:trackPublication:didUpdateStatistics:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:trackPublication:didUpdateStatistics:)
*Instance Method*
Track 診斷統計更新(每秒一次,僅 `reportStatistics == true` 時 fire、且有變化才 fire)。 與 `ParticipantDelegate.didUpdateStatistics` 雙 fire、與 `@Published track.statistics` 同源。
```swift
func room(_ room: Room, participant: Participant, trackPublication: TrackPublication, didUpdateStatistics statistics: TrackStatistics)
```
---
# room(_:participant:trackPublication:didUpdateStatistics:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participant:trackPublication:didUpdateStatistics:)
*Instance Method*
```swift
func room(_ room: Room, participant: Participant, trackPublication: TrackPublication, didUpdateStatistics statistics: TrackStatistics)
```
---
# room(_:participantDidConnect:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participantDidConnect:)
*Instance Method*
參與者加入房間(對方接受邀請後 fire)
```swift
func room(_ room: Room, participantDidConnect participant: RemoteParticipant)
```
## Discussion
觸發時機:對方端 `FriendManager.acceptInvitation` 成功 → ToxAV callState=active → Room 建立 RemoteParticipant + 加入 `remoteParticipants` 字典 → fire 此 callback。
---
# room(_:participantDidConnect:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participantDidConnect:)
*Instance Method*
```swift
func room(_ room: Room, participantDidConnect participant: RemoteParticipant)
```
---
# room(_:participantDidDisconnect:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participantDidDisconnect:)
*Instance Method*
參與者離開房間
```swift
func room(_ room: Room, participantDidDisconnect participant: RemoteParticipant)
```
## Discussion
觸發時機(任一):
- 對方主動掛斷(ToxAV finished)→ `participant.lastDisconnectReason = .leave`
- 對方網路錯誤 / 本端寬限期到期(ToxAV error)→ `.networkLost`
- 本地主動 `Room.shared.disconnectAll()` / `participant.disconnect()` → `.leave`
⚠️ **連線路徑全斷(paths bitmap 歸零)不在此列** —— revuekit-c 0.17.0 契約 明定「bitmap 歸零不蘊含 session 已結束」,空窗期間 C 層仍在重連。 該情境改由 `didUpdatePaths` 通知(見下),participant 保留至 ToxAV 宣告結束為止。
App 在此 callback 內可讀 `participant.lastDisconnectReason` 區分掛斷原因。
---
# room(_:participantDidDisconnect:)
[RevueSDK](../../index.md) › [RoomDelegate](../index.md) › room(_:participantDidDisconnect:)
*Instance Method*
```swift
func room(_ room: Room, participantDidDisconnect participant: RemoteParticipant)
```
---
# RoomError.alreadyConnected
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.alreadyConnected
*Case*
已在房間中(重複 connect)
```swift
case alreadyConnected
```
---
# RoomError.alreadyRecording
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.alreadyRecording
*Case*
已在錄影中
```swift
case alreadyRecording
```
---
# RoomError.audioPublicationAlreadyExists(friendId:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.audioPublicationAlreadyExists(friendId:)
*Case*
該好友的 audio 單流名額已佔(同一好友同時只能有一道 audio 發佈)
```swift
case audioPublicationAlreadyExists(friendId: String)
```
---
# RoomError.broadcastExtensionNotConfigured
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.broadcastExtensionNotConfigured
*Case*
Broadcast Extension 未設定
```swift
case broadcastExtensionNotConfigured
```
---
# RoomError.callAlreadyActive
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.callAlreadyActive
*Case*
通話已在進行中
```swift
case callAlreadyActive
```
---
# RoomError.callNotConnected
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.callNotConnected
*Case*
無法撥打(未連線)
```swift
case callNotConnected
```
---
# RoomError.callRejected
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.callRejected
*Case*
通話被對方拒絕
```swift
case callRejected
```
---
# RoomError.callTimeout
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.callTimeout
*Case*
通話超時未接
```swift
case callTimeout
```
---
# RoomError.cameraAccessDenied
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.cameraAccessDenied
*Case*
相機存取被拒
```swift
case cameraAccessDenied
```
---
# RoomError.cameraDeviceNotFound
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.cameraDeviceNotFound
*Case*
相機裝置找不到
```swift
case cameraDeviceNotFound
```
---
# RoomError.cancelled
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.cancelled
*Case*
操作被取消
```swift
case cancelled
```
---
# RoomError.capturerNotReady
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.capturerNotReady
*Case*
擷取器尚未準備好
```swift
case capturerNotReady
```
---
# RoomError.codecNotSupported(codec:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.codecNotSupported(codec:)
*Case*
不支援的 Codec
```swift
case codecNotSupported(codec: String)
```
---
# RoomError.connectionFailed(reason:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.connectionFailed(reason:)
*Case*
連線失敗
```swift
case connectionFailed(reason: String)
```
---
# RoomError.dataPublishFailed(reason:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.dataPublishFailed(reason:)
*Case*
資料發送失敗
```swift
case dataPublishFailed(reason: String)
```
---
# RoomError.disconnected
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.disconnected
*Case*
已斷線(操作需要連線狀態)
```swift
case disconnected
```
---
# Error Implementations
[RevueSDK](../../index.md) › [RoomError](../index.md) › Error Implementations
*Article*
---
# errorDescription
[RevueSDK](../../index.md) › [RoomError](../index.md) › errorDescription
*Instance Property*
```swift
var errorDescription: String? { get }
```
---
# failureReason
[RevueSDK](../../index.md) › [RoomError](../index.md) › failureReason
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var failureReason: String? { get }
```
---
# helpAnchor
[RevueSDK](../../index.md) › [RoomError](../index.md) › helpAnchor
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var helpAnchor: String? { get }
```
---
# RoomError
[RevueSDK](../index.md) › RoomError
*Enumeration*
SDK 錯誤類型
```swift
enum RoomError
```
## Topics
### Enumeration Cases
- [`case alreadyConnected`](alreadyconnected/index.md)
已在房間中(重複 connect)
- [`case alreadyRecording`](alreadyrecording/index.md)
已在錄影中
- [`case audioPublicationAlreadyExists(friendId: String)`](audiopublicationalreadyexists%28friendid:%29/index.md)
該好友的 audio 單流名額已佔(同一好友同時只能有一道 audio 發佈)
- [`case broadcastExtensionNotConfigured`](broadcastextensionnotconfigured/index.md)
Broadcast Extension 未設定
- [`case callAlreadyActive`](callalreadyactive/index.md)
通話已在進行中
- [`case callNotConnected`](callnotconnected/index.md)
無法撥打(未連線)
- [`case callRejected`](callrejected/index.md)
通話被對方拒絕
- [`case callTimeout`](calltimeout/index.md)
通話超時未接
- [`case cameraAccessDenied`](cameraaccessdenied/index.md)
相機存取被拒
- [`case cameraDeviceNotFound`](cameradevicenotfound/index.md)
相機裝置找不到
- [`case cancelled`](cancelled/index.md)
操作被取消
- [`case capturerNotReady`](capturernotready/index.md)
擷取器尚未準備好
- [`case codecNotSupported(codec: String)`](codecnotsupported%28codec:%29/index.md)
不支援的 Codec
- [`case connectionFailed(reason: String)`](connectionfailed%28reason:%29/index.md)
連線失敗
- [`case dataPublishFailed(reason: String)`](datapublishfailed%28reason:%29/index.md)
資料發送失敗
- [`case disconnected`](disconnected/index.md)
已斷線(操作需要連線狀態)
- [`case initializationFailed(reason: String)`](initializationfailed%28reason:%29/index.md)
SDK 初始化失敗
- [`case insufficientStorage`](insufficientstorage/index.md)
儲存空間不足
- [`case invalidFriendId`](invalidfriendid/index.md)
無效的 Friend ID
- [`case invalidParameter(name: String, reason: String)`](invalidparameter%28name:reason:%29/index.md)
無效參數
- [`case mediaInitializationFailed(reason: String)`](mediainitializationfailed%28reason:%29/index.md)
媒體初始化失敗
- [`case microphoneAccessDenied`](microphoneaccessdenied/index.md)
麥克風存取被拒
- [`case networkError(underlying: Error?)`](networkerror%28underlying:%29/index.md)
網路錯誤
- [`case noIncomingCall`](noincomingcall/index.md)
無來電可接聽
- [`case notInitialized`](notinitialized/index.md)
SDK 尚未初始化
- [`case notRecording`](notrecording/index.md)
未在錄影中
- [`case publishNotAllowed`](publishnotallowed/index.md)
無發佈權限
- [`case reconnectionFailed`](reconnectionfailed/index.md)
重連失敗
- [`case recordingWriteFailed(reason: String)`](recordingwritefailed%28reason:%29/index.md)
錄影寫入失敗
- [`case roomFull`](roomfull/index.md)
房間已滿
- [`case roomNotFound(roomId: String)`](roomnotfound%28roomid:%29/index.md)
房間不存在
- [`case rpcMethodNotFound(method: String)`](rpcmethodnotfound%28method:%29/index.md)
RPC 方法未註冊
- [`case rpcTimeout(method: String)`](rpctimeout%28method:%29/index.md)
RPC 呼叫逾時
- [`case screenShareDenied`](screensharedenied/index.md)
螢幕分享被使用者拒絕
- [`case screenShareSystemRestriction`](screensharesystemrestriction/index.md)
螢幕分享被系統限制
- [`case subscribeNotAllowed`](subscribenotallowed/index.md)
無訂閱權限
- [`case timedOut`](timedout/index.md)
連線逾時
- [`case trackAlreadyPublished`](trackalreadypublished/index.md)
Track 已發佈(重複 publish)
- [`case trackNotPublished`](tracknotpublished/index.md)
Track 未發佈(unpublish 一個沒發佈的 Track)
- [`case unknown(reason: String)`](unknown%28reason:%29/index.md)
未知錯誤
- [`case videoPublicationAlreadyExists(friendId: String)`](videopublicationalreadyexists%28friendid:%29/index.md)
該好友的 video 單流名額已佔(同一好友同時只能有一道 video 發佈)
### Instance Properties
- [`var errorDescription: String?`](errordescription/index.md)
---
# RoomError.initializationFailed(reason:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.initializationFailed(reason:)
*Case*
SDK 初始化失敗
```swift
case initializationFailed(reason: String)
```
---
# RoomError.insufficientStorage
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.insufficientStorage
*Case*
儲存空間不足
```swift
case insufficientStorage
```
---
# RoomError.invalidFriendId
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.invalidFriendId
*Case*
無效的 Friend ID
```swift
case invalidFriendId
```
---
# RoomError.invalidParameter(name:reason:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.invalidParameter(name:reason:)
*Case*
無效參數
```swift
case invalidParameter(name: String, reason: String)
```
---
# localizedDescription
[RevueSDK](../../index.md) › [RoomError](../index.md) › localizedDescription
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var localizedDescription: String { get }
```
---
# LocalizedError Implementations
[RevueSDK](../../index.md) › [RoomError](../index.md) › LocalizedError Implementations
*Article*
---
# RoomError.mediaInitializationFailed(reason:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.mediaInitializationFailed(reason:)
*Case*
媒體初始化失敗
```swift
case mediaInitializationFailed(reason: String)
```
---
# RoomError.microphoneAccessDenied
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.microphoneAccessDenied
*Case*
麥克風存取被拒
```swift
case microphoneAccessDenied
```
---
# RoomError.networkError(underlying:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.networkError(underlying:)
*Case*
網路錯誤
```swift
case networkError(underlying: Error?)
```
---
# RoomError.noIncomingCall
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.noIncomingCall
*Case*
無來電可接聽
```swift
case noIncomingCall
```
---
# RoomError.notInitialized
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.notInitialized
*Case*
SDK 尚未初始化
```swift
case notInitialized
```
---
# RoomError.notRecording
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.notRecording
*Case*
未在錄影中
```swift
case notRecording
```
---
# RoomError.publishNotAllowed
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.publishNotAllowed
*Case*
無發佈權限
```swift
case publishNotAllowed
```
---
# RoomError.reconnectionFailed
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.reconnectionFailed
*Case*
重連失敗
```swift
case reconnectionFailed
```
---
# RoomError.recordingWriteFailed(reason:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.recordingWriteFailed(reason:)
*Case*
錄影寫入失敗
```swift
case recordingWriteFailed(reason: String)
```
---
# recoverySuggestion
[RevueSDK](../../index.md) › [RoomError](../index.md) › recoverySuggestion
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var recoverySuggestion: String? { get }
```
---
# RoomError.roomFull
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.roomFull
*Case*
房間已滿
```swift
case roomFull
```
---
# RoomError.roomNotFound(roomId:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.roomNotFound(roomId:)
*Case*
房間不存在
```swift
case roomNotFound(roomId: String)
```
---
# RoomError.rpcMethodNotFound(method:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.rpcMethodNotFound(method:)
*Case*
RPC 方法未註冊
```swift
case rpcMethodNotFound(method: String)
```
---
# RoomError.rpcTimeout(method:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.rpcTimeout(method:)
*Case*
RPC 呼叫逾時
```swift
case rpcTimeout(method: String)
```
---
# RoomError.screenShareDenied
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.screenShareDenied
*Case*
螢幕分享被使用者拒絕
```swift
case screenShareDenied
```
---
# RoomError.screenShareSystemRestriction
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.screenShareSystemRestriction
*Case*
螢幕分享被系統限制
```swift
case screenShareSystemRestriction
```
---
# RoomError.subscribeNotAllowed
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.subscribeNotAllowed
*Case*
無訂閱權限
```swift
case subscribeNotAllowed
```
---
# RoomError.timedOut
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.timedOut
*Case*
連線逾時
```swift
case timedOut
```
---
# RoomError.trackAlreadyPublished
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.trackAlreadyPublished
*Case*
Track 已發佈(重複 publish)
```swift
case trackAlreadyPublished
```
---
# RoomError.trackNotPublished
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.trackNotPublished
*Case*
Track 未發佈(unpublish 一個沒發佈的 Track)
```swift
case trackNotPublished
```
---
# RoomError.unknown(reason:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.unknown(reason:)
*Case*
未知錯誤
```swift
case unknown(reason: String)
```
---
# RoomError.videoPublicationAlreadyExists(friendId:)
[RevueSDK](../../index.md) › [RoomError](../index.md) › RoomError.videoPublicationAlreadyExists(friendId:)
*Case*
該好友的 video 單流名額已佔(同一好友同時只能有一道 video 發佈)
```swift
case videoPublicationAlreadyExists(friendId: String)
```
---
# RoomMessageError.encodingFailed
[RevueSDK](../../index.md) › [RoomMessageError](../index.md) › RoomMessageError.encodingFailed
*Case*
JSON envelope 編碼失敗
```swift
case encodingFailed
```
---
# Error Implementations
[RevueSDK](../../index.md) › [RoomMessageError](../index.md) › Error Implementations
*Article*
---
# errorDescription
[RevueSDK](../../index.md) › [RoomMessageError](../index.md) › errorDescription
*Instance Property*
```swift
var errorDescription: String? { get }
```
---
# failureReason
[RevueSDK](../../index.md) › [RoomMessageError](../index.md) › failureReason
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var failureReason: String? { get }
```
---
# helpAnchor
[RevueSDK](../../index.md) › [RoomMessageError](../index.md) › helpAnchor
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var helpAnchor: String? { get }
```
---
# RoomMessageError
[RevueSDK](../index.md) › RoomMessageError
*Enumeration*
Room 層訊息錯誤型別
```swift
enum RoomMessageError
```
## Overview
由 `LocalParticipant.publish(message:to:timeout:)` 丟出。
底層的 `FriendMessageError` 被 wrap 在 `.transportFailed` 裡, 使用者可用 `if case .transportFailed(let underlying)` 拆出細節。
## Topics
### Enumeration Cases
- [`case encodingFailed`](encodingfailed/index.md)
JSON envelope 編碼失敗
- [`case messageTooLong(size: Int, maxSize: Int)`](messagetoolong%28size:maxsize:%29/index.md)
envelope 總長超過 toxcore 單筆上限
- [`case transportFailed(underlying: FriendMessageError)`](transportfailed%28underlying:%29/index.md)
底層 FriendManager 失敗,原樣封裝
- [`case unsupportedSource(Track.Source)`](unsupportedsource%28_:%29/index.md)
`LocalParticipant.set(source:enabled:)` 收到不支援的 source 值 (例如 `.unknown` / `.screenShareAudio` 等本 API 未涵蓋的來源)
---
# localizedDescription
[RevueSDK](../../index.md) › [RoomMessageError](../index.md) › localizedDescription
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var localizedDescription: String { get }
```
---
# LocalizedError Implementations
[RevueSDK](../../index.md) › [RoomMessageError](../index.md) › LocalizedError Implementations
*Article*
---
# RoomMessageError.messageTooLong(size:maxSize:)
[RevueSDK](../../index.md) › [RoomMessageError](../index.md) › RoomMessageError.messageTooLong(size:maxSize:)
*Case*
envelope 總長超過 toxcore 單筆上限
```swift
case messageTooLong(size: Int, maxSize: Int)
```
---
# recoverySuggestion
[RevueSDK](../../index.md) › [RoomMessageError](../index.md) › recoverySuggestion
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var recoverySuggestion: String? { get }
```
---
# RoomMessageError.transportFailed(underlying:)
[RevueSDK](../../index.md) › [RoomMessageError](../index.md) › RoomMessageError.transportFailed(underlying:)
*Case*
底層 FriendManager 失敗,原樣封裝
```swift
case transportFailed(underlying: FriendMessageError)
```
---
# RoomMessageError.unsupportedSource(_:)
[RevueSDK](../../index.md) › [RoomMessageError](../index.md) › RoomMessageError.unsupportedSource(_:)
*Case*
`LocalParticipant.set(source:enabled:)` 收到不支援的 source 值 (例如 `.unknown` / `.screenShareAudio` 等本 API 未涵蓋的來源)
```swift
case unsupportedSource(Track.Source)
```
---
# appAudio
[RevueSDK](../../index.md) › [ScreenShareCaptureOptions](../index.md) › appAudio
*Instance Property*
是否擷取 App 音訊(預設 false) Broadcast 模式:使用者切到別的 App 時、可擷取那個 App 的音訊
```swift
var appAudio: Bool
```
---
# defaultToBroadcastExtension
[RevueSDK](../../index.md) › [ScreenShareCaptureOptions](../index.md) › defaultToBroadcastExtension
*Type Property*
靜態動態偵測:App 是否配了 Broadcast Extension iOS:由 `BroadcastBundleInfo.hasExtension` 動態算出 macOS:一律 false
```swift
static let defaultToBroadcastExtension: Bool
```
---
# dimensions
[RevueSDK](../../index.md) › [ScreenShareCaptureOptions](../index.md) › dimensions
*Instance Property*
⚠️ **iOS 為宣告性 hint、不生效**:iOS 螢幕分享(ReplayKit / in-app)走 buffer 注入, frame 原尺寸直通,實際尺寸以擷取到的畫面為準。 macOS(ScreenCaptureKit)未來實作時,此值會作為 aspectFit 縮放上限生效
```swift
var dimensions: CameraCaptureOptions.Dimensions
```
---
# displayId
[RevueSDK](../../index.md) › [ScreenShareCaptureOptions](../index.md) › displayId
*Instance Property*
指定螢幕 ID(macOS 專屬) TODO(macOS): macOS 多螢幕支援用;iOS 一律 nil
```swift
var displayId: UInt32?
```
---
# excludeWindowIDs
[RevueSDK](../../index.md) › [ScreenShareCaptureOptions](../index.md) › excludeWindowIDs
*Instance Property*
排除視窗 ID(macOS 專屬)
```swift
var excludeWindowIDs: [UInt32]
```
---
# fps
[RevueSDK](../../index.md) › [ScreenShareCaptureOptions](../index.md) › fps
*Instance Property*
⚠️ **iOS 為宣告性 hint**:實際幀率由系統擷取回調頻率決定。macOS ScreenCaptureKit 未來實作時會用此值設 `SCStreamConfiguration.minimumFrameInterval`。
```swift
var fps: Int
```
---
# includeCurrentApplication
[RevueSDK](../../index.md) › [ScreenShareCaptureOptions](../index.md) › includeCurrentApplication
*Instance Property*
是否包含自己 App 畫面(macOS 專屬、預設 true)
```swift
var includeCurrentApplication: Bool
```
---
# ScreenShareCaptureOptions
[RevueSDK](../index.md) › ScreenShareCaptureOptions
*Class*
螢幕分享擷取選項
```swift
class ScreenShareCaptureOptions
```
## Overview
```swift
let options = ScreenShareCaptureOptions(
dimensions: .h1080_169,
fps: 30,
appAudio: true
)
localParticipant.setScreenShare(enabled: true, options: options)
```
## Topics
### Initializers
- [`init(dimensions: CameraCaptureOptions.Dimensions, fps: Int, showCursor: Bool, appAudio: Bool, useBroadcastExtension: Bool, includeCurrentApplication: Bool, excludeWindowIDs: [UInt32], displayId: UInt32?)`](init%28dimensions:fps:showcursor:appaudio:usebroadcastextension:includecurrentapplication:excludewindowids:displayid:%29/index.md)
### Instance Properties
- [`var appAudio: Bool`](appaudio/index.md)
是否擷取 App 音訊(預設 false) Broadcast 模式:使用者切到別的 App 時、可擷取那個 App 的音訊
- [`var dimensions: CameraCaptureOptions.Dimensions`](dimensions/index.md)
⚠️ **iOS 為宣告性 hint、不生效**:iOS 螢幕分享(ReplayKit / in-app)走 buffer 注入, frame 原尺寸直通,實際尺寸以擷取到的畫面為準。 macOS(ScreenCaptureKit)未來實作時,此值會作為 aspectFit 縮放上限生效
- [`var displayId: UInt32?`](displayid/index.md)
指定螢幕 ID(macOS 專屬) TODO(macOS): macOS 多螢幕支援用;iOS 一律 nil
- [`var excludeWindowIDs: [UInt32]`](excludewindowids/index.md)
排除視窗 ID(macOS 專屬)
- [`var fps: Int`](fps/index.md)
⚠️ **iOS 為宣告性 hint**:實際幀率由系統擷取回調頻率決定。macOS ScreenCaptureKit 未來實作時會用此值設 `SCStreamConfiguration.minimumFrameInterval`。
- [`var includeCurrentApplication: Bool`](includecurrentapplication/index.md)
是否包含自己 App 畫面(macOS 專屬、預設 true)
- [`var showCursor: Bool`](showcursor/index.md)
是否顯示游標(macOS 專屬、預設 true)
- [`var useBroadcastExtension: Bool`](usebroadcastextension/index.md)
是否使用 Broadcast Extension(iOS) 預設由 `defaultToBroadcastExtension` 靜態偵測「App 有沒有配 Extension」決定
### Type Properties
- [`static let defaultToBroadcastExtension: Bool`](defaulttobroadcastextension/index.md)
靜態動態偵測:App 是否配了 Broadcast Extension iOS:由 `BroadcastBundleInfo.hasExtension` 動態算出 macOS:一律 false
---
# init(dimensions:fps:showCursor:appAudio:useBroadcastExtension:includeCurrentApplication:excludeWindowIDs:displayId:)
[RevueSDK](../../index.md) › [ScreenShareCaptureOptions](../index.md) › init(dimensions:fps:showCursor:appAudio:useBroadcastExtension:includeCurrentApplication:excludeWindowIDs:displayId:)
*Initializer*
```swift
init(dimensions: CameraCaptureOptions.Dimensions = .h1080_169, fps: Int = 30, showCursor: Bool = true, appAudio: Bool = false, useBroadcastExtension: Bool = defaultToBroadcastExtension, includeCurrentApplication: Bool = true, excludeWindowIDs: [UInt32] = [], displayId: UInt32? = nil)
```
---
# showCursor
[RevueSDK](../../index.md) › [ScreenShareCaptureOptions](../index.md) › showCursor
*Instance Property*
是否顯示游標(macOS 專屬、預設 true)
```swift
var showCursor: Bool
```
---
# useBroadcastExtension
[RevueSDK](../../index.md) › [ScreenShareCaptureOptions](../index.md) › useBroadcastExtension
*Instance Property*
是否使用 Broadcast Extension(iOS) 預設由 `defaultToBroadcastExtension` 靜態偵測「App 有沒有配 Extension」決定
```swift
var useBroadcastExtension: Bool
```
## Discussion
- true: 使用 ReplayKit Broadcast Extension(可分享任何 App)
- false: 使用 In-App 擷取(只能分享自己的 App)
---
# Error Implementations
[RevueSDK](../../index.md) › [ScreenShareError](../index.md) › Error Implementations
*Article*
---
# ScreenShareError.extensionLaunchFailed(underlying:)
[RevueSDK](../../index.md) › [ScreenShareError](../index.md) › ScreenShareError.extensionLaunchFailed(underlying:)
*Case*
Broadcast Extension 啟動失敗
```swift
case extensionLaunchFailed(underlying: Error)
```
---
# ScreenShareError.extensionNotConfigured
[RevueSDK](../../index.md) › [ScreenShareError](../index.md) › ScreenShareError.extensionNotConfigured
*Case*
想走 Broadcast 但 App 沒配 Extension (這是防禦性檢查、正常流程 `defaultToBroadcastExtension` 會 fallback 到 In-app)
```swift
case extensionNotConfigured
```
---
# ScreenShareError
[RevueSDK](../index.md) › ScreenShareError
*Enumeration*
螢幕分享錯誤型別
```swift
enum ScreenShareError
```
## Topics
### Enumeration Cases
- [`case extensionLaunchFailed(underlying: Error)`](extensionlaunchfailed%28underlying:%29/index.md)
Broadcast Extension 啟動失敗
- [`case extensionNotConfigured`](extensionnotconfigured/index.md)
想走 Broadcast 但 App 沒配 Extension (這是防禦性檢查、正常流程 `defaultToBroadcastExtension` 會 fallback 到 In-app)
- [`case ipcConnectionFailed(underlying: Error)`](ipcconnectionfailed%28underlying:%29/index.md)
Unix Socket 連線失敗
- [`case notImplemented(feature: String, plannedVersion: String)`](notimplemented%28feature:plannedversion:%29/index.md)
功能尚未實作(例如 macOS ScreenCaptureKit)
- [`case notInRoom`](notinroom/index.md)
未在通話中
- [`case systemDenied`](systemdenied/index.md)
使用者拒絕螢幕錄影權限(In-app 模式)
- [`case userCancelled`](usercancelled/index.md)
使用者取消系統對話框(Broadcast picker / In-app 螢幕錄影權限)
---
# ScreenShareError.ipcConnectionFailed(underlying:)
[RevueSDK](../../index.md) › [ScreenShareError](../index.md) › ScreenShareError.ipcConnectionFailed(underlying:)
*Case*
Unix Socket 連線失敗
```swift
case ipcConnectionFailed(underlying: Error)
```
---
# localizedDescription
[RevueSDK](../../index.md) › [ScreenShareError](../index.md) › localizedDescription
*Instance Property*
- iOS 8.0+
- iPadOS 8.0+
- macCatalyst 8.0+
- macOS 10.10+
- tvOS 9.0+
- watchOS 2.0+
```swift
var localizedDescription: String { get }
```
---
# ScreenShareError.notImplemented(feature:plannedVersion:)
[RevueSDK](../../index.md) › [ScreenShareError](../index.md) › ScreenShareError.notImplemented(feature:plannedVersion:)
*Case*
功能尚未實作(例如 macOS ScreenCaptureKit)
```swift
case notImplemented(feature: String, plannedVersion: String)
```
## Parameters
- feature
未實作的功能名稱(給錯誤訊息用)
- plannedVersion
預計實作的 SDK 版本(給錯誤訊息用)
---
# ScreenShareError.notInRoom
[RevueSDK](../../index.md) › [ScreenShareError](../index.md) › ScreenShareError.notInRoom
*Case*
未在通話中
```swift
case notInRoom
```
---
# ScreenShareError.systemDenied
[RevueSDK](../../index.md) › [ScreenShareError](../index.md) › ScreenShareError.systemDenied
*Case*
使用者拒絕螢幕錄影權限(In-app 模式)
```swift
case systemDenied
```
---
# ScreenShareError.userCancelled
[RevueSDK](../../index.md) › [ScreenShareError](../index.md) › ScreenShareError.userCancelled
*Case*
使用者取消系統對話框(Broadcast picker / In-app 螢幕錄影權限)
```swift
case userCancelled
```
---
# changeSavedataPassword(path:oldPassword:newPassword:)
[RevueSDK](../../index.md) › [SecurityManager](../index.md) › changeSavedataPassword(path:oldPassword:newPassword:)
*Instance Method*
```swift
func changeSavedataPassword(path: String, oldPassword: String, newPassword: String) -> Bool
```
---
# getFriendFingerprint(friendId:)
[RevueSDK](../../index.md) › [SecurityManager](../index.md) › getFriendFingerprint(friendId:)
*Instance Method*
```swift
func getFriendFingerprint(friendId: String) -> String?
```
---
# getFriendRevueKitSessionInfo(friendId:)
[RevueSDK](../../index.md) › [SecurityManager](../index.md) › getFriendRevueKitSessionInfo(friendId:)
*Instance Method*
```swift
func getFriendRevueKitSessionInfo(friendId: String) -> RevueKitSessionInfo?
```
---
# getRevueKitCryptoInfo()
[RevueSDK](../../index.md) › [SecurityManager](../index.md) › getRevueKitCryptoInfo()
*Instance Method*
```swift
func getRevueKitCryptoInfo() -> RevueKitCryptoInfo
```
---
# SecurityManager
[RevueSDK](../index.md) › SecurityManager
*Class*
安全管理器
```swift
final class SecurityManager
```
## Overview
提供加密演算法資訊查詢、好友會話安全資訊、指紋驗證、 存檔加密/解密等安全功能。
## Topics
### Instance Methods
- [`func changeSavedataPassword(path: String, oldPassword: String, newPassword: String) -> Bool`](changesavedatapassword%28path:oldpassword:newpassword:%29/index.md)
- [`func getFriendFingerprint(friendId: String) -> String?`](getfriendfingerprint%28friendid:%29/index.md)
- [`func getFriendRevueKitSessionInfo(friendId: String) -> RevueKitSessionInfo?`](getfriendrevuekitsessioninfo%28friendid:%29/index.md)
- [`func getRevueKitCryptoInfo() -> RevueKitCryptoInfo`](getrevuekitcryptoinfo%28%29/index.md)
- [`func isSavedataEncrypted(path: String) -> Bool`](issavedataencrypted%28path:%29/index.md)
- [`func verifySafetyNumber(friendId: String, expected: String) -> Bool`](verifysafetynumber%28friendid:expected:%29/index.md)
---
# isSavedataEncrypted(path:)
[RevueSDK](../../index.md) › [SecurityManager](../index.md) › isSavedataEncrypted(path:)
*Instance Method*
```swift
func isSavedataEncrypted(path: String) -> Bool
```
---
# verifySafetyNumber(friendId:expected:)
[RevueSDK](../../index.md) › [SecurityManager](../index.md) › verifySafetyNumber(friendId:expected:)
*Instance Method*
```swift
func verifySafetyNumber(friendId: String, expected: String) -> Bool
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [SelfConnectionStatus](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [SelfConnectionStatus](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../index.md) › [SelfConnectionStatus](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [SelfConnectionStatus](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# SelfConnectionStatus
[RevueSDK](../index.md) › SelfConnectionStatus
*Enumeration*
自身與 DHT 網路的連線狀態(對應 C 層 `RevueKitConnectionStatus`)
```swift
enum SelfConnectionStatus
```
## Overview
注意:這是自己與 DHT 網路的連線狀態,不是好友之間的連線狀態。 好友連線路徑請使用 `FriendConnectionPaths` (v0.13.0-rc1 起為三條 path bitmap)。
raw value 對應 C 層 `revuekit_get_connection_status()` 回傳值。
## Topics
### Enumeration Cases
- [`case offline`](offline/index.md)
離線(無法連接任何 DHT 節點)
- [`case tcp`](tcp/index.md)
TCP 連線(透過 TCP Relay 連接)
- [`case udp`](udp/index.md)
UDP 連線(直接 DHT 連接,最佳)
### Initializers
- [`init?(rawValue: Int)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../index.md) › [SelfConnectionStatus](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [SelfConnectionStatus](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# SelfConnectionStatus.offline
[RevueSDK](../../index.md) › [SelfConnectionStatus](../index.md) › SelfConnectionStatus.offline
*Case*
離線(無法連接任何 DHT 節點)
```swift
case offline
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [SelfConnectionStatus](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [SelfConnectionStatus](../index.md) › RawRepresentable Implementations
*Article*
---
# SelfConnectionStatus.tcp
[RevueSDK](../../index.md) › [SelfConnectionStatus](../index.md) › SelfConnectionStatus.tcp
*Case*
TCP 連線(透過 TCP Relay 連接)
```swift
case tcp
```
---
# SelfConnectionStatus.udp
[RevueSDK](../../index.md) › [SelfConnectionStatus](../index.md) › SelfConnectionStatus.udp
*Case*
UDP 連線(直接 DHT 連接,最佳)
```swift
case udp
```
---
# aspectRatio
[RevueSDK](../../index.md) › [Track](../index.md) › aspectRatio
*Instance Property*
寬高比(僅 video Track 有值)
```swift
var aspectRatio: CGFloat { get }
```
---
# cameraName
[RevueSDK](../../index.md) › [Track](../index.md) › cameraName
*Type Property*
```swift
static let cameraName: String
```
---
# delegates
[RevueSDK](../../index.md) › [Track](../index.md) › delegates
*Instance Property*
軌道事件代理(`TrackDelegate`):mute 翻轉等軌道本身的事件,在 main thread 觸發。
```swift
let delegates: MulticastDelegate
```
---
# dimensions
[RevueSDK](../../index.md) › [Track](../index.md) › dimensions
*Instance Property*
視訊解析度(僅 video Track 有值)
```swift
@Published var dimensions: CMVideoDimensions? { get set }
```
## Discussion
- 遠端:`RemoteVideoTrack.handleReceived` 從 pixelBuffer 讀取後更新
- 本地:尚未實作(LocalVideoTrack capture 路徑未更新此值)
---
# id
[RevueSDK](../../index.md) › [Track](../index.md) › id
*Instance Property*
```swift
var id: String { get }
```
---
# Track
[RevueSDK](../index.md) › Track
*Class*
Track 基礎類別 — 一路獨立的媒體串流
```swift
class Track
```
## Overview
Track 是 SDK 的核心概念,代表一路視訊或音訊串流。 可以想像成「水管」,可以接到不同的 VideoView(水龍頭)上。
每個 Track 都知道自己的狀態(靜音、解析度、FPS 等), 且永遠屬於某個 Participant。
## Topics
### Instance Properties
- [`var aspectRatio: CGFloat`](aspectratio/index.md)
寬高比(僅 video Track 有值)
- [`let delegates: MulticastDelegate`](delegates/index.md)
軌道事件代理(`TrackDelegate`):mute 翻轉等軌道本身的事件,在 main thread 觸發。
- [`var dimensions: CMVideoDimensions?`](dimensions/index.md)
視訊解析度(僅 video Track 有值)
- [`var id: String`](id/index.md)
- [`var isMuted: Bool`](ismuted/index.md)
是否靜音(軌道層:資料有沒有在流,所有接收者一起受影響)
- [`let kind: Kind`](kind-swift.property/index.md)
Track 種類(audio / video)
- [`let name: String`](name/index.md)
Track 名稱
- [`let sid: String`](sid/index.md)
Track Session ID
- [`let source: Source`](source-swift.property/index.md)
Track 來源(camera / microphone / screenShare)
- [`var statistics: TrackStatistics?`](statistics/index.md)
Track 連線診斷統計(每秒由 `Room` 採集 timer 更新)
- [`var trackState: TrackState`](trackstate-swift.property/index.md)
當前狀態
### Type Properties
- [`static let cameraName: String`](cameraname/index.md)
- [`static let microphoneName: String`](microphonename/index.md)
- [`static let screenShareAudioName: String`](screenshareaudioname/index.md)
- [`static let screenShareVideoName: String`](screensharevideoname/index.md)
### Enumerations
- [`enum Kind`](kind-swift.enum/index.md)
Track 種類
- [`enum Source`](source-swift.enum/index.md)
Track 來源
- [`enum TrackState`](trackstate-swift.enum/index.md)
Track 狀態
## Relationships
### Inherited By
- [`VideoTrack`](../videotrack/index.md)
- [`AudioTrack`](../audiotrack/index.md)
---
# isMuted
[RevueSDK](../../index.md) › [Track](../index.md) › isMuted
*Instance Property*
是否靜音(軌道層:資料有沒有在流,所有接收者一起受影響)
```swift
@Published var isMuted: Bool { get set }
```
## Discussion
翻轉時觸發 `TrackDelegate.track(_:didUpdateIsMuted:)`。每一份 publication 自己的 mute 狀態看 `TrackPublication.isMuted`,兩者互不改寫。
---
# !=(_:_:)
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Kind](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Track.Kind.audio
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Kind](../index.md) › Track.Kind.audio
*Case*
```swift
case audio
```
---
# Equatable Implementations
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Kind](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Kind](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Kind](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# Track.Kind
[RevueSDK](../../index.md) › [Track](../index.md) › Track.Kind
*Enumeration*
Track 種類
```swift
enum Kind
```
## Topics
### Enumeration Cases
- [`case audio`](audio/index.md)
- [`case video`](video/index.md)
### Initializers
- [`init?(rawValue: Int)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Kind](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Kind](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Kind](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Kind](../index.md) › RawRepresentable Implementations
*Article*
---
# Track.Kind.video
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Kind](../index.md) › Track.Kind.video
*Case*
```swift
case video
```
---
# kind
[RevueSDK](../../index.md) › [Track](../index.md) › kind
*Instance Property*
Track 種類(audio / video)
```swift
let kind: Kind
```
---
# microphoneName
[RevueSDK](../../index.md) › [Track](../index.md) › microphoneName
*Type Property*
```swift
static let microphoneName: String
```
---
# name
[RevueSDK](../../index.md) › [Track](../index.md) › name
*Instance Property*
Track 名稱
```swift
let name: String
```
---
# objectWillChange
[RevueSDK](../../index.md) › [Track](../index.md) › objectWillChange
*Instance Property*
- iOS 13.0+
- iPadOS 13.0+
- macCatalyst 13.0+
- macOS 10.15+
- tvOS 13.0+
- watchOS 6.0+
```swift
var objectWillChange: ObservableObjectPublisher { get }
```
---
# ObservableObject Implementations
[RevueSDK](../../index.md) › [Track](../index.md) › ObservableObject Implementations
*Article*
---
# screenShareAudioName
[RevueSDK](../../index.md) › [Track](../index.md) › screenShareAudioName
*Type Property*
```swift
static let screenShareAudioName: String
```
---
# screenShareVideoName
[RevueSDK](../../index.md) › [Track](../index.md) › screenShareVideoName
*Type Property*
```swift
static let screenShareVideoName: String
```
---
# sid
[RevueSDK](../../index.md) › [Track](../index.md) › sid
*Instance Property*
Track Session ID
```swift
let sid: String
```
---
# !=(_:_:)
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Track.Source.camera
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › Track.Source.camera
*Case*
```swift
case camera
```
---
# Equatable Implementations
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# Track.Source
[RevueSDK](../../index.md) › [Track](../index.md) › Track.Source
*Enumeration*
Track 來源
```swift
enum Source
```
## Topics
### Enumeration Cases
- [`case camera`](camera/index.md)
- [`case microphone`](microphone/index.md)
- [`case screenShareAudio`](screenshareaudio/index.md)
- [`case screenShareVideo`](screensharevideo/index.md)
- [`case unknown`](unknown/index.md)
### Initializers
- [`init?(rawValue: String)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: String)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# Track.Source.microphone
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › Track.Source.microphone
*Case*
```swift
case microphone
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › RawRepresentable Implementations
*Article*
---
# Track.Source.screenShareAudio
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › Track.Source.screenShareAudio
*Case*
```swift
case screenShareAudio
```
---
# Track.Source.screenShareVideo
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › Track.Source.screenShareVideo
*Case*
```swift
case screenShareVideo
```
---
# Track.Source.unknown
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.Source](../index.md) › Track.Source.unknown
*Case*
```swift
case unknown
```
---
# source
[RevueSDK](../../index.md) › [Track](../index.md) › source
*Instance Property*
Track 來源(camera / microphone / screenShare)
```swift
let source: Source
```
---
# statistics
[RevueSDK](../../index.md) › [Track](../index.md) › statistics
*Instance Property*
Track 連線診斷統計(每秒由 `Room` 採集 timer 更新)
```swift
@Published var statistics: TrackStatistics? { get set }
```
## Discussion
僅在 `Room.shared.reportStatistics == true` 時採集;關閉時停 timer 省電、此值不再更新。 分桶結構見 `TrackStatistics`:codec / videoSource / iceCandidatePair / inboundRtp / outboundRtp。
---
# !=(_:_:)
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.TrackState](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.TrackState](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.TrackState](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.TrackState](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# Track.TrackState
[RevueSDK](../../index.md) › [Track](../index.md) › Track.TrackState
*Enumeration*
Track 狀態
```swift
enum TrackState
```
## Topics
### Enumeration Cases
- [`case started`](started/index.md)
- [`case stopped`](stopped/index.md)
### Initializers
- [`init?(rawValue: Int)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.TrackState](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.TrackState](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.TrackState](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.TrackState](../index.md) › RawRepresentable Implementations
*Article*
---
# Track.TrackState.started
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.TrackState](../index.md) › Track.TrackState.started
*Case*
```swift
case started
```
---
# Track.TrackState.stopped
[RevueSDK](../../../index.md) › [Track](../../index.md) › [Track.TrackState](../index.md) › Track.TrackState.stopped
*Case*
```swift
case stopped
```
---
# trackState
[RevueSDK](../../index.md) › [Track](../index.md) › trackState
*Instance Property*
當前狀態
```swift
@Published var trackState: TrackState { get set }
```
---
# TrackDelegate
[RevueSDK](../index.md) › TrackDelegate
*Protocol*
軌道事件代理 — 訂閱單一 `Track` 本身的事件
```swift
protocol TrackDelegate : AnyObject
```
## Overview
作用在整條軌道、所有接收者一起受影響,所以從這裡回報;某一份 publication 被 `LocalTrackPublication.mute()`/`unmute()` 時,改由 `ParticipantDelegate.participant(_:trackPublication:didUpdateIsMuted:)` 回報那一份。
事件一律在 main thread 觸發(同 `ParticipantDelegate`)。
使用方式:
```swift
Room.shared.localParticipant.cameraTrack.delegates.add(delegate: self)
```
所有方法都有預設空實作,可選擇性實作需要的事件。
## Topics
### Instance Methods
- [`func track(Track, didUpdateIsMuted: Bool)`](track%28_:didupdateismuted:%29/index.md)
軌道的 `isMuted` 翻轉時觸發一次(與這條軌道發布給幾個人無關)。
---
# track(_:didUpdateIsMuted:)
[RevueSDK](../../index.md) › [TrackDelegate](../index.md) › track(_:didUpdateIsMuted:)
*Instance Method*
軌道的 `isMuted` 翻轉時觸發一次(與這條軌道發布給幾個人無關)。
```swift
func track(_ track: Track, didUpdateIsMuted isMuted: Bool)
```
---
# track(_:didUpdateIsMuted:)
[RevueSDK](../../index.md) › [TrackDelegate](../index.md) › track(_:didUpdateIsMuted:)
*Instance Method*
```swift
func track(_ track: Track, didUpdateIsMuted isMuted: Bool)
```
---
# dimensions
[RevueSDK](../../index.md) › [TrackPublication](../index.md) › dimensions
*Instance Property*
解析度(僅 video)
```swift
var dimensions: CMVideoDimensions? { get }
```
---
# id
[RevueSDK](../../index.md) › [TrackPublication](../index.md) › id
*Instance Property*
```swift
var id: String { get }
```
---
# TrackPublication
[RevueSDK](../index.md) › TrackPublication
*Class*
Track 發佈狀態 — 包裝 Track 的發佈/訂閱資訊
```swift
class TrackPublication
```
## Overview
當 Track 被發佈到房間後,會產生一個 TrackPublication。 透過 `participant.trackPublications` 存取。
## Topics
### Instance Properties
- [`var dimensions: CMVideoDimensions?`](dimensions/index.md)
解析度(僅 video)
- [`var id: String`](id/index.md)
- [`var isMuted: Bool`](ismuted/index.md)
是否靜音
- [`var isSubscribed: Bool`](issubscribed/index.md)
P2P 沒有伺服器可以允許或拒絕訂閱:本地 publication 在發布時就有 track, 遠端 publication 在收到宣告時與 track 一起建立,所以拿得到的 publication 都是 true; track 清掉後回 false。只由 `track` 的 `didSet` 更新,不另外寫入。
- [`let kind: Track.Kind`](kind/index.md)
Track 種類
- [`var mimeType: String?`](mimetype/index.md)
使用中的編碼格式(`"video/VP8"`、`"video/H264"`、`"audio/opus"`)
- [`let name: String`](name/index.md)
Track 名稱
- [`let sid: String`](sid/index.md)
Publication Session ID
- [`let source: Track.Source`](source/index.md)
Track 來源
- [`var track: Track?`](track/index.md)
對應的 Track 物件
## Relationships
### Inherited By
- [`RemoteTrackPublication`](../remotetrackpublication/index.md)
- [`LocalTrackPublication`](../localtrackpublication/index.md)
---
# isMuted
[RevueSDK](../../index.md) › [TrackPublication](../index.md) › isMuted
*Instance Property*
是否靜音
```swift
@Published var isMuted: Bool { get set }
```
---
# isSubscribed
[RevueSDK](../../index.md) › [TrackPublication](../index.md) › isSubscribed
*Instance Property*
P2P 沒有伺服器可以允許或拒絕訂閱:本地 publication 在發布時就有 track, 遠端 publication 在收到宣告時與 track 一起建立,所以拿得到的 publication 都是 true; track 清掉後回 false。只由 `track` 的 `didSet` 更新,不另外寫入。
```swift
@Published var isSubscribed: Bool { get set }
```
---
# kind
[RevueSDK](../../index.md) › [TrackPublication](../index.md) › kind
*Instance Property*
Track 種類
```swift
let kind: Track.Kind
```
---
# mimeType
[RevueSDK](../../index.md) › [TrackPublication](../index.md) › mimeType
*Instance Property*
使用中的編碼格式(`"video/VP8"`、`"video/H264"`、`"audio/opus"`)
```swift
@Published var mimeType: String? { get set }
```
## Discussion
由 SDK 內部填入、對外唯讀:
- 本地 publication:發布成功時填入實際送出的 codec,`setCodec(_:)` 切換後更新。
- 遠端 publication:第一幀到達前是 nil(codec 從收到的資料判定,宣告不帶 codec), 依該幀走哪條接收 callback 填入,對方切換 codec 時更新。
- 音訊固定 `"audio/opus"`(revuekit-c 預設 `REVUEKIT_AUDIO_CODEC_OPUS`,SDK 不改 preferred audio codec)。
設值一律在 main(apple/0006):遠端寫入點在接收佇列,由 `applyReceivedVideoCodec` 切 main。
---
# name
[RevueSDK](../../index.md) › [TrackPublication](../index.md) › name
*Instance Property*
Track 名稱
```swift
let name: String
```
---
# objectWillChange
[RevueSDK](../../index.md) › [TrackPublication](../index.md) › objectWillChange
*Instance Property*
- iOS 13.0+
- iPadOS 13.0+
- macCatalyst 13.0+
- macOS 10.15+
- tvOS 13.0+
- watchOS 6.0+
```swift
var objectWillChange: ObservableObjectPublisher { get }
```
---
# ObservableObject Implementations
[RevueSDK](../../index.md) › [TrackPublication](../index.md) › ObservableObject Implementations
*Article*
---
# sid
[RevueSDK](../../index.md) › [TrackPublication](../index.md) › sid
*Instance Property*
Publication Session ID
```swift
let sid: String
```
---
# source
[RevueSDK](../../index.md) › [TrackPublication](../index.md) › source
*Instance Property*
Track 來源
```swift
let source: Track.Source
```
---
# track
[RevueSDK](../../index.md) › [TrackPublication](../index.md) › track
*Instance Property*
對應的 Track 物件
```swift
@Published var track: Track? { get set }
```
## Discussion
指派或清掉時同步更新 `isSubscribed`(有 track 就是 true)。
---
# audioTrack
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › audioTrack
*Instance Property*
要錄製的 AudioTrack(nil = 只錄視訊無聲)
```swift
let audioTrack: AudioTrack?
```
---
# availableDiskSpace()
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › availableDiskSpace()
*Type Method*
查詢當前可用磁碟空間(bytes)
```swift
static func availableDiskSpace() -> Int64
```
## Discussion
使用 `volumeAvailableCapacityForImportantUsageKey`,Apple 會自動回收其他 App 的 cache 讓重要用途使用,拿到的是真實可用數字。
---
# deleteAllRecordings()
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › deleteAllRecordings()
*Type Method*
刪除所有錄影檔
```swift
static func deleteAllRecordings() throws
```
---
# deleteRecording(at:)
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › deleteRecording(at:)
*Type Method*
刪除指定路徑的錄影檔
```swift
static func deleteRecording(at url: URL) throws
```
---
# deleteRecording(fileId:)
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › deleteRecording(fileId:)
*Type Method*
刪除指定錄影檔(by ID)
```swift
static func deleteRecording(fileId: String) throws
```
---
# diskSpaceCriticalThreshold
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › diskSpaceCriticalThreshold
*Type Property*
磁碟空間臨界閾值(預設 50 MB)
```swift
static var diskSpaceCriticalThreshold: Int64
```
---
# diskSpaceWarning
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › diskSpaceWarning
*Instance Property*
磁碟空間警告事件(Task 3 實作)
```swift
@Published var diskSpaceWarning: DiskSpaceWarning? { get set }
```
---
# diskSpaceWarningThreshold
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › diskSpaceWarningThreshold
*Type Property*
磁碟空間警告閾值(預設 100 MB)
```swift
static var diskSpaceWarningThreshold: Int64
```
---
# getRecordingList()
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › getRecordingList()
*Type Method*
取得所有錄影檔列表
```swift
static func getRecordingList() -> [RecordingFileInfo]
```
---
# TrackRecorder
[RevueSDK](../index.md) › TrackRecorder
*Class*
Track 錄影器 — 將單一 VideoTrack + 單一 AudioTrack 錄製為 fMP4 檔案
```swift
class TrackRecorder
```
## Overview
獨立物件,不綁定在 Room 或 Participant 上。 可同時建立多個 TrackRecorder 各自錄不同內容。
多路混音由外部 `AudioCompositor` 負責,多路合成由外部 `VideoCompositor` 負責, TrackRecorder 只接單一 track(混音/合成後的 outputTrack)。
```swift
// === 錄自己(單一路)===
let recorder = TrackRecorder(
videoTrack: localParticipant.firstCameraVideoTrack,
audioTrack: localParticipant.firstAudioTrack
)
try recorder.startRecording(fileName: "self")
// === 錄完整會議(外部先合成再錄)===
let mix = AudioCompositor()
try mix.start()
mix.addTrack(localParticipant.firstAudioTrack!)
mix.addTrack(participantA.firstAudioTrack!)
let recorder = TrackRecorder(
videoTrack: videoCompositor.outputTrack,
audioTrack: mix.outputTrack,
videoFps: 30,
videoSize: CGSize(width: 1920, height: 1080)
)
try recorder.startRecording(fileName: "meeting")
// === 只錄音訊 ===
let audioOnly = TrackRecorder(videoTrack: nil, audioTrack: mic)
// === 只錄視訊無聲 ===
let videoOnly = TrackRecorder(videoTrack: screen, audioTrack: nil)
```
## Topics
### Initializers
- [`init(videoTrack: VideoTrack?, audioTrack: AudioTrack?, videoFps: UInt, videoSize: CGSize)`](init%28videotrack:audiotrack:videofps:videosize:%29/index.md)
建立 TrackRecorder
### Instance Properties
- [`let audioTrack: AudioTrack?`](audiotrack/index.md)
要錄製的 AudioTrack(nil = 只錄視訊無聲)
- [`var diskSpaceWarning: DiskSpaceWarning?`](diskspacewarning/index.md)
磁碟空間警告事件(Task 3 實作)
- [`var isRecording: Bool`](isrecording/index.md)
是否錄影中(便捷屬性)
- [`var lastError: Error?`](lasterror/index.md)
錄影中途錯誤(Task 4 實作) 開始時的錯誤走 `startRecording` throws,這裡只處理錄影過程中的非同步錯誤
- [`var lastFinishedFile: RecordingFileInfo?`](lastfinishedfile/index.md)
最近一次寫完的錄影檔(finalize 完成才設值)
- [`var state: RecorderState`](state/index.md)
錄影狀態(轉發 engine.state)
- [`let videoTrack: VideoTrack?`](videotrack/index.md)
要錄製的 VideoTrack(nil = 只錄音訊)
### Instance Methods
- [`func pauseRecording()`](pauserecording%28%29/index.md)
暫停錄影
- [`func resumeRecording()`](resumerecording%28%29/index.md)
繼續錄影
- [`func startRecording(fileName: String) throws`](startrecording%28filename:%29/index.md)
開始錄影
- [`func stopRecording(completion: ((RecordingFileInfo?) -> Void)?)`](stoprecording%28completion:%29/index.md)
停止錄影(engine 在背景 finalize,不阻塞呼叫端)
### Type Properties
- [`static var diskSpaceCriticalThreshold: Int64`](diskspacecriticalthreshold/index.md)
磁碟空間臨界閾值(預設 50 MB)
- [`static var diskSpaceWarningThreshold: Int64`](diskspacewarningthreshold/index.md)
磁碟空間警告閾值(預設 100 MB)
- [`static var recordingsDirectory: URL`](recordingsdirectory/index.md)
錄影檔存放目錄(Documents/Recordings/)
### Type Methods
- [`static func availableDiskSpace() -> Int64`](availablediskspace%28%29/index.md)
查詢當前可用磁碟空間(bytes)
- [`static func deleteAllRecordings() throws`](deleteallrecordings%28%29/index.md)
刪除所有錄影檔
- [`static func deleteRecording(at: URL) throws`](deleterecording%28at:%29/index.md)
刪除指定路徑的錄影檔
- [`static func deleteRecording(fileId: String) throws`](deleterecording%28fileid:%29/index.md)
刪除指定錄影檔(by ID)
- [`static func getRecordingList() -> [RecordingFileInfo]`](getrecordinglist%28%29/index.md)
取得所有錄影檔列表
---
# init(videoTrack:audioTrack:videoFps:videoSize:)
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › init(videoTrack:audioTrack:videoFps:videoSize:)
*Initializer*
建立 TrackRecorder
```swift
init(videoTrack: VideoTrack? = nil, audioTrack: AudioTrack? = nil, videoFps: UInt = 20, videoSize: CGSize = CGSize(width: 1280, height: 720))
```
## Parameters
- videoTrack
要錄製的視訊 Track(nil = 只錄音訊)
- audioTrack
要錄製的音訊 Track(nil = 只錄視訊無聲)
- videoFps
錄影幀率(預設 20)
- videoSize
錄影解析度(預設 1280×720)
---
# isRecording
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › isRecording
*Instance Property*
是否錄影中(便捷屬性)
```swift
var isRecording: Bool { get }
```
---
# lastError
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › lastError
*Instance Property*
錄影中途錯誤(Task 4 實作) 開始時的錯誤走 `startRecording` throws,這裡只處理錄影過程中的非同步錯誤
```swift
@Published var lastError: Error? { get set }
```
---
# lastFinishedFile
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › lastFinishedFile
*Instance Property*
最近一次寫完的錄影檔(finalize 完成才設值)
```swift
@Published var lastFinishedFile: RecordingFileInfo? { get set }
```
## Discussion
涵蓋**全部三條停止路徑**:主動 `stopRecording()`、engine 錯誤自動停止、 磁碟臨界自動停止。後兩者是 SDK 內部自行停止、呼叫方沒有 completion 可收, 只能靠訂閱這個屬性得知檔案已產出。
檔案寫入失敗時不設值(維持前一次的值)。
---
# objectWillChange
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › objectWillChange
*Instance Property*
- iOS 13.0+
- iPadOS 13.0+
- macCatalyst 13.0+
- macOS 10.15+
- tvOS 13.0+
- watchOS 6.0+
```swift
var objectWillChange: ObservableObjectPublisher { get }
```
---
# ObservableObject Implementations
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › ObservableObject Implementations
*Article*
---
# pauseRecording()
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › pauseRecording()
*Instance Method*
暫停錄影
```swift
func pauseRecording()
```
## Discussion
不取消 Track 訂閱,engine 的 fillVideo/fillAudio 內部 guard `state == .recording`, paused 時自動丟棄傳入的 buffer。
---
# recordingsDirectory
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › recordingsDirectory
*Type Property*
錄影檔存放目錄(Documents/Recordings/)
```swift
static var recordingsDirectory: URL { get }
```
---
# resumeRecording()
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › resumeRecording()
*Instance Method*
繼續錄影
```swift
func resumeRecording()
```
---
# startRecording(fileName:)
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › startRecording(fileName:)
*Instance Method*
開始錄影
```swift
func startRecording(fileName: String) throws
```
## Parameters
- fileName
檔案名稱(不含副檔名,自動加 .mp4)
## Discussion
> **Throws**
>
> - `RecordingError.insufficientDiskSpace` 當磁碟可用空間 < criticalThreshold
> - engine 內部錯誤(檔名衝突、AVAssetWriter 建立失敗等)
---
# state
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › state
*Instance Property*
錄影狀態(轉發 engine.state)
```swift
var state: RecorderState { get }
```
---
# stopRecording(completion:)
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › stopRecording(completion:)
*Instance Method*
停止錄影(engine 在背景 finalize,不阻塞呼叫端)
```swift
func stopRecording(completion: ((RecordingFileInfo?) -> Void)? = nil)
```
## Parameters
- completion
檔案**實際寫入完成後**呼叫(main queue),帶出剛寫完的 錄影檔資訊;停止時非錄影中、或檔案寫入失敗時帶 `nil`。
## Discussion
本方法立即返回,實際的 fMP4 收尾(`AVAssetWriter.finishWriting`)在背景進行, 期間 `state` 為 `.finalizing`,完成後轉 `.idle`。**檔案在 completion 觸發前 不可讀取**。
completion 只服務主動呼叫者。engine 錯誤與磁碟臨界的自動停止不經過此參數, 需訂閱 [`lastFinishedFile`](../lastfinishedfile/index.md) 才收得到(三條路徑都會更新該屬性)。
---
# videoTrack
[RevueSDK](../../index.md) › [TrackRecorder](../index.md) › videoTrack
*Instance Property*
要錄製的 VideoTrack(nil = 只錄音訊)
```swift
let videoTrack: VideoTrack?
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [TrackStatistics](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# codec
[RevueSDK](../../index.md) › [TrackStatistics](../index.md) › codec
*Instance Property*
```swift
let codec: CodecStatistics?
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [TrackStatistics](../index.md) › Equatable Implementations
*Article*
---
# iceCandidatePair
[RevueSDK](../../index.md) › [TrackStatistics](../index.md) › iceCandidatePair
*Instance Property*
```swift
let iceCandidatePair: IceCandidatePairStatistics?
```
---
# inboundRtp
[RevueSDK](../../index.md) › [TrackStatistics](../index.md) › inboundRtp
*Instance Property*
```swift
let inboundRtp: InboundStatistics?
```
---
# TrackStatistics
[RevueSDK](../index.md) › TrackStatistics
*Structure*
只保留「有料且對診斷有用」的桶(收斂記錄見 spec):
```swift
struct TrackStatistics
```
## Overview
- `codec`:當前實際視訊編碼(VP8/H264)
- `videoSource`:發端相機源尺寸/fps(編碼前)
- `iceCandidatePair`:連線品質(rtt/loss/jitter/bitrate/收發量)
- `inboundRtp`:收端主力(解析度/fps/幀健康/凍結/音訊)
- `outboundRtp`:發端送出規格(width/height/fps)
`Equatable` 供 timer「有變化才 fire delegate」判定(避免無變化時洗版 delegate)。
## Topics
### Instance Properties
- [`let codec: CodecStatistics?`](codec/index.md)
- [`let iceCandidatePair: IceCandidatePairStatistics?`](icecandidatepair/index.md)
- [`let inboundRtp: InboundStatistics?`](inboundrtp/index.md)
- [`let outboundRtp: OutboundStatistics?`](outboundrtp/index.md)
- [`let videoSource: VideoSourceStatistics?`](videosource/index.md)
---
# outboundRtp
[RevueSDK](../../index.md) › [TrackStatistics](../index.md) › outboundRtp
*Instance Property*
```swift
let outboundRtp: OutboundStatistics?
```
---
# videoSource
[RevueSDK](../../index.md) › [TrackStatistics](../index.md) › videoSource
*Instance Property*
```swift
let videoSource: VideoSourceStatistics?
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [UserStatus](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# UserStatus.away
[RevueSDK](../../index.md) › [UserStatus](../index.md) › UserStatus.away
*Case*
離開
```swift
case away
```
---
# UserStatus.busy
[RevueSDK](../../index.md) › [UserStatus](../index.md) › UserStatus.busy
*Case*
忙碌
```swift
case busy
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [UserStatus](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../index.md) › [UserStatus](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [UserStatus](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# UserStatus
[RevueSDK](../index.md) › UserStatus
*Enumeration*
使用者狀態(對應 C 層 `RevueKitUserStatus`)
```swift
enum UserStatus
```
## Overview
toxcore 層級的使用者狀態,會透過 P2P 廣播給所有好友。 raw value 對應 C 層 `revuekit_self_set_status()` 參數。
## Topics
### Enumeration Cases
- [`case away`](away/index.md)
離開
- [`case busy`](busy/index.md)
忙碌
- [`case none`](none/index.md)
在線(預設)
### Initializers
- [`init?(rawValue: Int32)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../index.md) › [UserStatus](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int32)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [UserStatus](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# UserStatus.none
[RevueSDK](../../index.md) › [UserStatus](../index.md) › UserStatus.none
*Case*
在線(預設)
```swift
case none
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [UserStatus](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [UserStatus](../index.md) › RawRepresentable Implementations
*Article*
---
# captureState
[RevueSDK](../../index.md) › [VideoCapturer](../index.md) › captureState
*Instance Property*
擷取器狀態
```swift
var captureState: CaptureState { get }
```
## Discussion
多執行緒同時存取(spec #86):相機在 main、buffer 在 App 呼叫的執行緒、 螢幕分享在 ReplayKit 回呼寫;`BufferCapturer.capture` 的守門 (App 注入的執行緒)讀 → 鎖保護。
---
# delegates
[RevueSDK](../../index.md) › [VideoCapturer](../index.md) › delegates
*Instance Property*
事件訂閱者(weak 參照,物件釋放即自動移除)
```swift
let delegates: MulticastDelegate
```
---
# dimensions
[RevueSDK](../../index.md) › [VideoCapturer](../index.md) › dimensions
*Instance Property*
當前實際解析度(採集/注入的真實尺寸,非設定值)
```swift
var dimensions: CMVideoDimensions? { get }
```
## Discussion
每幀由擷取執行緒寫(相機擷取佇列、buffer 注入的執行緒、螢幕分享), App 與 Flutter 外掛在 main 讀 → 鎖保護,做法同 `captureState`。
---
# VideoCapturer
[RevueSDK](../index.md) › VideoCapturer
*Class*
視訊擷取器基底類別
```swift
class VideoCapturer
```
## Overview
protocol 改制)。改制理由:`captureState` / `dimensions` / `outputSubject` 原本在四個 capturer 各自重複宣告,事件通知也就沒有「只寫一次」的地方; 改為基底類別後共用狀態集中於此,`setState` / `setDimensions` 內含去重與 事件派送,新增 capturer 自動具備事件能力。
`.started` 事件(呼叫即生效語意);本類別不這麼做,而是由子類別自行決定 呼叫 `setState` 的時機 —— `InAppScreenCapturer` 的 `captureState` 必須 「只在 ReplayKit completionHandler 內翻」以反映系統真實狀態(見該檔註解), 與「呼叫意圖」解耦,若由基底代發會退回它修正前的行為。
子類別必須覆寫 `startCapture()` / `stopCapture()`,並在適當時機呼叫 `setState` / `setDimensions` / `emit`。
## Topics
### Instance Properties
- [`var captureState: CaptureState`](capturestate/index.md)
擷取器狀態
- [`let delegates: MulticastDelegate`](delegates/index.md)
事件訂閱者(weak 參照,物件釋放即自動移除)
- [`var dimensions: CMVideoDimensions?`](dimensions/index.md)
當前實際解析度(採集/注入的真實尺寸,非設定值)
### Instance Methods
- [`func startCapture() async throws`](startcapture%28%29/index.md)
啟動擷取(子類別必須覆寫)
- [`func stopCapture() async throws`](stopcapture%28%29/index.md)
停止擷取(子類別必須覆寫)
## Relationships
### Inherited By
- [`BufferCapturer`](../buffercapturer/index.md)
- [`CameraCapturer`](../cameracapturer/index.md)
---
# startCapture()
[RevueSDK](../../index.md) › [VideoCapturer](../index.md) › startCapture()
*Instance Method*
啟動擷取(子類別必須覆寫)
```swift
func startCapture() async throws
```
---
# stopCapture()
[RevueSDK](../../index.md) › [VideoCapturer](../index.md) › stopCapture()
*Instance Method*
停止擷取(子類別必須覆寫)
```swift
func stopCapture() async throws
```
---
# capturer(_:didDowngradeResolution:to:)
[RevueSDK](../../index.md) › [VideoCapturerDelegate](../index.md) › capturer(_:didDowngradeResolution:to:)
*Instance Method*
採集格式**無法滿足要求的解析度** —— 於挑選格式當下判定。
```swift
func capturer(_ capturer: VideoCapturer, didDowngradeResolution requested: CMVideoDimensions, to actual: CMVideoDimensions)
```
## Discussion
**與 `didUpdate dimensions:` 的差異**(兩者不可互相取代):
- `dimensions` 回報「實際輸出尺寸」,且**只在值變化時** fire(去重)。
- 本 callback 回報「要求 vs 鏡頭能力」的落差,**即使實際輸出沒變也會 fire**。
後者才是「降級」的定義。典型情境:前鏡頭已在 1080p,使用者把設定調到 4K —— 鏡頭撐不住、實際輸出仍是 1080p(尺寸沒變 → 無 `dimensions` 事件), 但這正是最該告知使用者的時刻。
每次套用格式都會判定(開視訊 / 切鏡頭 / 改設定),故可能在使用者未主動要求時 也 fire —— SDK 忠實回報事實,「何時該打擾使用者」由上層決定。
---
# capturer(_:didDowngradeResolution:to:)
[RevueSDK](../../index.md) › [VideoCapturerDelegate](../index.md) › capturer(_:didDowngradeResolution:to:)
*Instance Method*
```swift
func capturer(_ capturer: VideoCapturer, didDowngradeResolution requested: CMVideoDimensions, to actual: CMVideoDimensions)
```
---
# capturer(_:didUpdate:)
[RevueSDK](../../index.md) › [VideoCapturerDelegate](../index.md) › capturer(_:didUpdate:)
*Instance Method*
```swift
func capturer(_ capturer: VideoCapturer, didUpdate dimensions: CMVideoDimensions?)
```
---
# capturer(_:didUpdate:)
[RevueSDK](../../index.md) › [VideoCapturerDelegate](../index.md) › capturer(_:didUpdate:)
*Instance Method*
實際採集解析度已變更
```swift
func capturer(_ capturer: VideoCapturer, didUpdate dimensions: CMVideoDimensions?)
```
## Discussion
回報**真正送出去的尺寸**,非 options 要求值 —— 鏡頭不支援要求的解析度時 會自動挑最接近的格式,且輸出不放大(目標解析度是上限語意),故兩者常不相等。 `nil` 表示尚未開始採集或已停止。
---
# capturer(_:didUpdate:)
[RevueSDK](../../index.md) › [VideoCapturerDelegate](../index.md) › capturer(_:didUpdate:)
*Instance Method*
```swift
func capturer(_ capturer: VideoCapturer, didUpdate state: CaptureState)
```
---
# capturer(_:didUpdate:)
[RevueSDK](../../index.md) › [VideoCapturerDelegate](../index.md) › capturer(_:didUpdate:)
*Instance Method*
擷取狀態已變更(idle / capturing / error)
```swift
func capturer(_ capturer: VideoCapturer, didUpdate state: CaptureState)
```
---
# VideoCapturerDelegate
[RevueSDK](../index.md) › VideoCapturerDelegate
*Protocol*
擷取器事件代理 — 訂閱解析度/狀態變更
```swift
protocol VideoCapturerDelegate : AnyObject
```
## Overview
訂閱者只需實作關心的事件。
三個事件一律在 main thread 觸發,可直接在事件裡更新畫面。
```swift
capturer.delegates.add(delegate: self)
func capturer(_ capturer: VideoCapturer, didUpdate dimensions: CMVideoDimensions?) {
print("實際解析度:\(dimensions?.width ?? 0)x\(dimensions?.height ?? 0)")
}
```
## Topics
### Instance Methods
- [`func capturer(VideoCapturer, didDowngradeResolution: CMVideoDimensions, to: CMVideoDimensions)`](capturer%28_:diddowngraderesolution:to:%29/index.md)
採集格式**無法滿足要求的解析度** —— 於挑選格式當下判定。
- [`func capturer(VideoCapturer, didUpdate: CMVideoDimensions?)`](capturer%28_:didupdate:%29-4ppoi/index.md)
實際採集解析度已變更
- [`func capturer(VideoCapturer, didUpdate: CaptureState)`](capturer%28_:didupdate:%29-81wu6/index.md)
擷取狀態已變更(idle / capturing / error)
---
# VideoCodec.h264(_:)
[RevueSDK](../../index.md) › [VideoCodec](../index.md) › VideoCodec.h264(_:)
*Case*
H.264 — 走外部硬體編碼(VideoToolbox),輸出 Annex B 送給 `send_frame_encoded`
```swift
case h264(H264Encoding)
```
---
# h264Default
[RevueSDK](../../index.md) › [VideoCodec](../index.md) › h264Default
*Type Property*
H.264 + 預設 bitrate(3 Mbps)
```swift
static let h264Default: VideoCodec
```
---
# VideoCodec
[RevueSDK](../index.md) › VideoCodec
*Enumeration*
視訊編碼器(含對應 encoding 參數)
```swift
enum VideoCodec
```
## Overview
enum with associated value — codec 跟 encoding 綁在一起,型別系統保證不會出現 「codec 是 h264、但傳 vp8 encoding」的矛盾組合。
## 5 個 static preset 常數
```swift
VideoCodec.h264Default // H.264 + 預設 bitrate 3 Mbps
VideoCodec.vp8LowLatency // VP8 + 低延遲 preset (CBR)
VideoCodec.vp8HighQuality // VP8 + 高畫質 preset (VBR)
VideoCodec.vp8LowBandwidth // VP8 + 弱網優先 preset (CQ)
VideoCodec.vp8AllKeyframe // VP8 + 每幀 keyframe(不破圖)
```
## Topics
### Enumeration Cases
- [`case h264(H264Encoding)`](h264%28_:%29/index.md)
H.264 — 走外部硬體編碼(VideoToolbox),輸出 Annex B 送給 `send_frame_encoded`
- [`case vp8(Vp8Encoding)`](vp8%28_:%29/index.md)
VP8 — 走 revuekit-c 內部軟體編碼,raw YUV 送給 `send_frame`
### Instance Properties
- [`var type: VideoCodecType`](type/index.md)
取出 codec type(忽略 associated encoding),用於 map key / 比對 codec 類型
### Type Properties
- [`static let h264Default: VideoCodec`](h264default/index.md)
H.264 + 預設 bitrate(3 Mbps)
- [`static let vp8AllKeyframe: VideoCodec`](vp8allkeyframe/index.md)
VP8 + 全 keyframe(每幀都是完整畫面、不破圖)
- [`static let vp8HighQuality: VideoCodec`](vp8highquality/index.md)
VP8 + 高畫質 preset(對應 C 層 `apply_high_quality_mode`)
- [`static let vp8LowBandwidth: VideoCodec`](vp8lowbandwidth/index.md)
VP8 + 弱網優先 preset(對應 C 層 `apply_low_bandwidth_mode`)
- [`static let vp8LowLatency: VideoCodec`](vp8lowlatency/index.md)
VP8 + 低延遲 preset(對應 C 層 `apply_low_latency_mode`)
---
# type
[RevueSDK](../../index.md) › [VideoCodec](../index.md) › type
*Instance Property*
取出 codec type(忽略 associated encoding),用於 map key / 比對 codec 類型
```swift
var type: VideoCodecType { get }
```
## Discussion
```swift
let codec: VideoCodec = .vp8(Vp8Encoding())
codec.type == .vp8 // true
```
---
# VideoCodec.vp8(_:)
[RevueSDK](../../index.md) › [VideoCodec](../index.md) › VideoCodec.vp8(_:)
*Case*
VP8 — 走 revuekit-c 內部軟體編碼,raw YUV 送給 `send_frame`
```swift
case vp8(Vp8Encoding)
```
---
# vp8AllKeyframe
[RevueSDK](../../index.md) › [VideoCodec](../index.md) › vp8AllKeyframe
*Type Property*
VP8 + 全 keyframe(每幀都是完整畫面、不破圖)
```swift
static let vp8AllKeyframe: VideoCodec
```
## Discussion
bitrate 3 Mbps / cpuUsed 16 / quantizer 30~40 / kf 1 / VBR
用途:對「畫面破圖」極度敏感的場景(例如:控制訊號、關鍵操作畫面)。 代價:無法利用 VP8 的差異幀壓縮、頻寬使用效率降低。
---
# vp8HighQuality
[RevueSDK](../../index.md) › [VideoCodec](../index.md) › vp8HighQuality
*Type Property*
VP8 + 高畫質 preset(對應 C 層 `apply_high_quality_mode`)
```swift
static let vp8HighQuality: VideoCodec
```
## Discussion
bitrate 8 Mbps / cpuUsed 4 / quantizer 2~40 / kf 60 / VBR
---
# vp8LowBandwidth
[RevueSDK](../../index.md) › [VideoCodec](../index.md) › vp8LowBandwidth
*Type Property*
VP8 + 弱網優先 preset(對應 C 層 `apply_low_bandwidth_mode`)
```swift
static let vp8LowBandwidth: VideoCodec
```
## Discussion
bitrate 1.5 Mbps / cpuUsed 16 / quantizer 10~56 / kf 10 / CQ
---
# vp8LowLatency
[RevueSDK](../../index.md) › [VideoCodec](../index.md) › vp8LowLatency
*Type Property*
VP8 + 低延遲 preset(對應 C 層 `apply_low_latency_mode`)
```swift
static let vp8LowLatency: VideoCodec
```
## Discussion
bitrate 3 Mbps / cpuUsed 12 / quantizer 4~48 / kf 15 / CBR
---
# !=(_:_:)
[RevueSDK](../../index.md) › [VideoCodecType](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [VideoCodecType](../index.md) › Equatable Implementations
*Article*
---
# VideoCodecType.h264
[RevueSDK](../../index.md) › [VideoCodecType](../index.md) › VideoCodecType.h264
*Case*
```swift
case h264
```
---
# hash(into:)
[RevueSDK](../../index.md) › [VideoCodecType](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [VideoCodecType](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# VideoCodecType
[RevueSDK](../index.md) › VideoCodecType
*Enumeration*
視訊編碼器類型(不帶 encoding 的 lightweight tag)
```swift
enum VideoCodecType
```
## Overview
用途:需要 Hashable 的場景(如 map key、比對 codec 類型時忽略 encoding)。 `VideoCodec` 帶 associated value 不 Hashable,此 enum 就是它的「只留 case」版本。
## Topics
### Enumeration Cases
- [`case h264`](h264/index.md)
- [`case vp8`](vp8/index.md)
### Initializers
- [`init?(rawValue: String)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../index.md) › [VideoCodecType](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: String)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [VideoCodecType](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [VideoCodecType](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [VideoCodecType](../index.md) › RawRepresentable Implementations
*Article*
---
# VideoCodecType.vp8
[RevueSDK](../../index.md) › [VideoCodecType](../index.md) › VideoCodecType.vp8
*Case*
```swift
case vp8
```
---
# currentTemplate
[RevueSDK](../../index.md) › [VideoCompositor](../index.md) › currentTemplate
*Instance Property*
當前模板(唯讀)
```swift
@Published var currentTemplate: CompositeTemplate? { get set }
```
---
# fps
[RevueSDK](../../index.md) › [VideoCompositor](../index.md) › fps
*Instance Property*
合成幀率(預設 30 FPS)
```swift
var fps: Int { get set }
```
---
# getTemplate()
[RevueSDK](../../index.md) › [VideoCompositor](../index.md) › getTemplate()
*Instance Method*
取得當前模板
```swift
func getTemplate() -> CompositeTemplate?
```
## Return Value
當前合成模板,未設定時返回 nil
---
# VideoCompositor
[RevueSDK](../index.md) › VideoCompositor
*Class*
視訊合成器 — 將多路 VideoTrack 按模板合成為一路輸出(獨立工具)
```swift
class VideoCompositor
```
## Overview
不綁定 Room,用戶自己建立、設定、發布。
```swift
let compositor = VideoCompositor()
compositor.fps = 30
compositor.resolutionSize = CGSize(width: 1920, height: 1080)
compositor.start()
// 設定合成模板
let template = CompositeTemplate(trackPositions: [
VideoTrackPositionInfo(track: cameraTrack, normalizedRect: CGRect(x: 0, y: 0, width: 0.5, height: 1.0)),
VideoTrackPositionInfo(track: remoteTrackA, normalizedRect: CGRect(x: 0.5, y: 0, width: 0.5, height: 1.0)),
])
compositor.setTemplate(template)
// 預覽
videoView.track = compositor.outputTrack
// 手動發布
// Room.shared.localParticipant.publish(videoTrack: compositor.outputTrack)
// 停止
compositor.stop()
```
## Topics
### Initializers
- [`init()`](init%28%29/index.md)
### Instance Properties
- [`var currentTemplate: CompositeTemplate?`](currenttemplate/index.md)
當前模板(唯讀)
- [`var fps: Int`](fps/index.md)
合成幀率(預設 30 FPS)
- [`var isRunning: Bool`](isrunning/index.md)
合成器是否運行中(唯讀)
- [`var outputTrack: LocalVideoTrack?`](outputtrack/index.md)
合成輸出 Track(用於本地預覽或發布)
- [`var resolutionSize: CGSize`](resolutionsize/index.md)
合成輸出解析度(預設 1280×720) 合成解析度 = 發送解析度,不做額外縮放
### Instance Methods
- [`func getTemplate() -> CompositeTemplate?`](gettemplate%28%29/index.md)
取得當前模板
- [`func setTemplate(CompositeTemplate)`](settemplate%28_:%29/index.md)
設定合成模板
- [`func start()`](start%28%29/index.md)
啟動合成器
- [`func stop()`](stop%28%29/index.md)
停止合成器,釋放所有資源
---
# init()
[RevueSDK](../../index.md) › [VideoCompositor](../index.md) › init()
*Initializer*
```swift
init()
```
---
# isRunning
[RevueSDK](../../index.md) › [VideoCompositor](../index.md) › isRunning
*Instance Property*
合成器是否運行中(唯讀)
```swift
@Published var isRunning: Bool { get set }
```
---
# objectWillChange
[RevueSDK](../../index.md) › [VideoCompositor](../index.md) › objectWillChange
*Instance Property*
- iOS 13.0+
- iPadOS 13.0+
- macCatalyst 13.0+
- macOS 10.15+
- tvOS 13.0+
- watchOS 6.0+
```swift
var objectWillChange: ObservableObjectPublisher { get }
```
---
# ObservableObject Implementations
[RevueSDK](../../index.md) › [VideoCompositor](../index.md) › ObservableObject Implementations
*Article*
---
# outputTrack
[RevueSDK](../../index.md) › [VideoCompositor](../index.md) › outputTrack
*Instance Property*
合成輸出 Track(用於本地預覽或發布)
```swift
var outputTrack: LocalVideoTrack? { get }
```
## Discussion
```swift
videoView.track = compositor.outputTrack
// Room.shared.localParticipant.publish(videoTrack: compositor.outputTrack)
```
---
# resolutionSize
[RevueSDK](../../index.md) › [VideoCompositor](../index.md) › resolutionSize
*Instance Property*
合成輸出解析度(預設 1280×720) 合成解析度 = 發送解析度,不做額外縮放
```swift
var resolutionSize: CGSize { get set }
```
---
# setTemplate(_:)
[RevueSDK](../../index.md) › [VideoCompositor](../index.md) › setTemplate(_:)
*Instance Method*
設定合成模板
```swift
func setTemplate(_ template: CompositeTemplate)
```
## Parameters
- template
合成模板(trackPositions 順序 = 繪製圖層)
## Discussion
可連續呼叫切換模板,不需要先 clear。 模板裡的 Track 不需要 publish,合成器直接讀取每幀畫面。
```swift
compositor.setTemplate(template)
// → 下一幀自動使用新模板
```
---
# start()
[RevueSDK](../../index.md) › [VideoCompositor](../index.md) › start()
*Instance Method*
啟動合成器
```swift
func start()
```
## Discussion
- 建立 Metal 視訊合成管線(VideoCompositorEngine)
- 建立 outputTrack(LocalVideoTrack + BufferCapturer)
- 啟動合成定時器(依照 fps 設定)
- isRunning = true
---
# stop()
[RevueSDK](../../index.md) › [VideoCompositor](../index.md) › stop()
*Instance Method*
停止合成器,釋放所有資源
```swift
func stop()
```
## Discussion
- 停止合成定時器
- 釋放 Metal 資源
- 停止 outputTrack
- currentTemplate = nil
- isRunning = false
---
# !=(_:_:)
[RevueSDK](../../index.md) › [VideoContentMode](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [VideoContentMode](../index.md) › Equatable Implementations
*Article*
---
# VideoContentMode.fill
[RevueSDK](../../index.md) › [VideoContentMode](../index.md) › VideoContentMode.fill
*Case*
填滿 slot,超出部分裁切(預設,等同 UIView.contentMode.scaleAspectFill)
```swift
case fill
```
---
# VideoContentMode.fit
[RevueSDK](../../index.md) › [VideoContentMode](../index.md) › VideoContentMode.fit
*Case*
縮放至完整顯示,不足部分留黑邊(等同 UIView.contentMode.scaleAspectFit)
```swift
case fit
```
---
# VideoContentMode
[RevueSDK](../index.md) › VideoContentMode
*Enumeration*
單一 Track 的內容縮放模式
```swift
enum VideoContentMode
```
## Topics
### Enumeration Cases
- [`case fill`](fill/index.md)
填滿 slot,超出部分裁切(預設,等同 UIView.contentMode.scaleAspectFill)
- [`case fit`](fit/index.md)
縮放至完整顯示,不足部分留黑邊(等同 UIView.contentMode.scaleAspectFit)
---
# VideoProcessor
[RevueSDK](../index.md) › VideoProcessor
*Protocol*
視訊處理器協定 — 可選的視訊處理管線
```swift
protocol VideoProcessor : AnyObject
```
## Topics
### Instance Methods
- [`func process(pixelBuffer: CVPixelBuffer) -> CVPixelBuffer?`](process%28pixelbuffer:%29/index.md)
處理一幀視訊(例如背景模糊)
---
# process(pixelBuffer:)
[RevueSDK](../../index.md) › [VideoProcessor](../index.md) › process(pixelBuffer:)
*Instance Method*
處理一幀視訊(例如背景模糊)
```swift
func process(pixelBuffer: CVPixelBuffer) -> CVPixelBuffer?
```
## Return Value
處理後的 pixelBuffer,nil 表示丟棄此幀
## Discussion
在擷取執行緒呼叫(不是 main),每一幀一次;處理太久會拖慢送出的幀率。
---
# codec
[RevueSDK](../../index.md) › [VideoPublishOptions](../index.md) › codec
*Instance Property*
視訊編碼器 + 對應 encoding 參數(enum with associated value)
```swift
let codec: VideoCodec
```
## Discussion
兩個 case:
- `.h264(H264Encoding)` — H.264 走外部硬體編碼(VideoToolbox),輸出 Annex B
- `.vp8(Vp8Encoding)` — VP8 走 revuekit-c 內部軟體編碼,輸出 raw YUV → 內部編
預設 `.vp8AllKeyframe`(不破圖精神:每幀 keyframe、無破圖累積)。
---
# degradationPreference
[RevueSDK](../../index.md) › [VideoPublishOptions](../index.md) › degradationPreference
*Instance Property*
品質降級偏好 — 頻寬不足時優先保持什麼、犧牲什麼
```swift
let degradationPreference: DegradationPreference
```
## Discussion
⚠️ **本欄位目前保留 API 骨架、未實作**
- 使用者塞值不會生效
- 目前實際降級策略由 `Vp8Encoding.rateControlMode`(VBR/CBR/CQ)承擔
- 未來將對接 revuekit-c 的 `resize_allowed` 機制(弱網自動縮小解析度)
---
# VideoPublishOptions
[RevueSDK](../index.md) › VideoPublishOptions
*Structure*
視訊發佈選項 — 設定視訊 Track 的編碼參數
```swift
struct VideoPublishOptions
```
## Overview
描述「發佈這個 Track 時要用哪種 codec、多少 fps、什麼降級策略」。 此為**編碼發送層**設定,跟採集層 (`CameraCaptureOptions`) 分離:
- `CameraCaptureOptions.fps` = 相機硬體吐幀速率
- `VideoPublishOptions.maxFps` = 編碼器發送端節流上限
## 使用
```swift
// 預設:VP8 AllKeyframe(不破圖精神)、15 fps
try await localParticipant.publish(
videoTrack: track, to: friend,
options: VideoPublishOptions()
)
// 明確指定 VP8 low-latency preset
try await localParticipant.publish(
videoTrack: track, to: friend,
options: VideoPublishOptions(codec: .vp8LowLatency)
)
// 明確指定 H.264 + 自訂 bitrate
try await localParticipant.publish(
videoTrack: track, to: friend,
options: VideoPublishOptions(codec: .h264(H264Encoding(maxBitrate: 5_000_000)))
)
```
## 通話中動態調整
對已發佈的 track、在通話中改編碼參數,走 `LocalTrackPublication` 的 setter:
- `setCodec(_:)` — 切換 codec(含 encoding)
- `setMaxFps(_:)` — 調整 fps 上限
- `setVp8Encoding(_:)` — 只調 VP8 encoding(當前 codec 必須是 vp8)
- `setH264Encoding(_:)` — 只調 H.264 encoding(當前 codec 必須是 h264)
- `requestKeyframe()` — 強制送 keyframe(畫面破圖恢復用)
切換 codec **不會中斷通話** — 內部走「換 send pipeline」(`send_frame` vs `send_frame_encoded`)而非 SDP 重談。
## Topics
### Initializers
- [`init(codec: VideoCodec, maxFps: Int, degradationPreference: DegradationPreference)`](init%28codec:maxfps:degradationpreference:%29/index.md)
### Instance Properties
- [`let codec: VideoCodec`](codec/index.md)
視訊編碼器 + 對應 encoding 參數(enum with associated value)
- [`let degradationPreference: DegradationPreference`](degradationpreference/index.md)
品質降級偏好 — 頻寬不足時優先保持什麼、犧牲什麼
- [`let maxFps: Int`](maxfps/index.md)
編碼器發送端 FPS 上限(預設 15)
---
# init(codec:maxFps:degradationPreference:)
[RevueSDK](../../index.md) › [VideoPublishOptions](../index.md) › init(codec:maxFps:degradationPreference:)
*Initializer*
```swift
init(codec: VideoCodec = .vp8AllKeyframe, maxFps: Int = 15, degradationPreference: DegradationPreference = .balanced)
```
---
# maxFps
[RevueSDK](../../index.md) › [VideoPublishOptions](../index.md) › maxFps
*Instance Property*
編碼器發送端 FPS 上限(預設 15)
```swift
let maxFps: Int
```
## Discussion
- 對應 C 層 `revuekit_av_video_set_max_fps`,SDK 軟體節流(丟幀),下一幀生效
- **跟 `CameraCaptureOptions.fps` 分離**:前者為採集、後者為編碼發送節流
- 若編碼 fps 上限高於採集 fps,實際就用採集 fps
- 若編碼 fps 上限低於採集 fps,內部丟幀節流
---
# VideoRenderer
[RevueSDK](../index.md) › VideoRenderer
*Protocol*
視訊渲染器協定 — VideoView 實作此協定接收畫面
```swift
protocol VideoRenderer : AnyObject
```
## Overview
只有 `render(pixelBuffer:)`。本地 mute 又沒有佔位畫面時,SDK 只對 `VideoView` 清畫面, 自訂 renderer 收不到這個通知。
## Topics
### Instance Methods
- [`func render(pixelBuffer: CVPixelBuffer)`](render%28pixelbuffer:%29/index.md)
渲染一幀視訊
## Relationships
### Conforming Types
- [`VideoView`](../videoview/index.md)
---
# render(pixelBuffer:)
[RevueSDK](../../index.md) › [VideoRenderer](../index.md) › render(pixelBuffer:)
*Instance Method*
渲染一幀視訊
```swift
func render(pixelBuffer: CVPixelBuffer)
```
---
# !=(_:_:)
[RevueSDK](../../index.md) › [VideoRotation](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# encode(to:)
[RevueSDK](../../index.md) › [VideoRotation](../index.md) › encode(to:)
*Instance Method*
```swift
func encode(to encoder: any Encoder) throws
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [VideoRotation](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../index.md) › [VideoRotation](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../index.md) › [VideoRotation](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# VideoRotation
[RevueSDK](../index.md) › VideoRotation
*Enumeration*
```swift
enum VideoRotation
```
## Overview
> **Note**
>
> Broadcast IPC 傳遞 frame 時,Extension 端把 ReplayKit 給的 `CGImagePropertyOrientation` 轉成此型別、隨 frame metadata 一起送給主 App 端。 純值型別、跨平台通用。
## Topics
### Initializers
- [`init?(rawValue: Int)`](init%28rawvalue:%29/index.md)
---
# init(from:)
[RevueSDK](../../index.md) › [VideoRotation](../index.md) › init(from:)
*Initializer*
```swift
init(from decoder: any Decoder) throws
```
---
# init(rawValue:)
[RevueSDK](../../index.md) › [VideoRotation](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../index.md) › [VideoRotation](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../index.md) › [VideoRotation](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../index.md) › [VideoRotation](../index.md) › RawRepresentable Implementations
*Article*
---
# !=(_:_:)
[RevueSDK](../../index.md) › [VideoSourceStatistics](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../index.md) › [VideoSourceStatistics](../index.md) › Equatable Implementations
*Article*
---
# fps
[RevueSDK](../../index.md) › [VideoSourceStatistics](../index.md) › fps
*Instance Property*
發送側來源實測幀率。 診斷:設定 30fps 但實測 12fps = 相機/螢幕採集受限(如光線暗自動降幀)。 來源:LocalVideoTrack output sink 數幀 / 秒(SDK 自算,非設定值)。
```swift
let fps: Double
```
---
# height
[RevueSDK](../../index.md) › [VideoSourceStatistics](../index.md) › height
*Instance Property*
```swift
let height: Int
```
---
# VideoSourceStatistics
[RevueSDK](../index.md) › VideoSourceStatistics
*Structure*
發送側來源畫面統計(編碼前)。
```swift
struct VideoSourceStatistics
```
## Topics
### Instance Properties
- [`let fps: Double`](fps/index.md)
發送側來源實測幀率。 診斷:設定 30fps 但實測 12fps = 相機/螢幕採集受限(如光線暗自動降幀)。 來源:LocalVideoTrack output sink 數幀 / 秒(SDK 自算,非設定值)。
- [`let height: Int`](height/index.md)
- [`let width: Int`](width/index.md)
發送側來源畫面寬/高(編碼前,相機/螢幕實際輸出尺寸)。 診斷:與 outboundRtp 比對——源頭 1080p 但送出 720p = 編碼/傳輸降質,非來源問題。 來源:LocalVideoTrack 的 capturer.output sink 讀 CVPixelBufferGetWidth/Height(實測值)。
---
# width
[RevueSDK](../../index.md) › [VideoSourceStatistics](../index.md) › width
*Instance Property*
發送側來源畫面寬/高(編碼前,相機/螢幕實際輸出尺寸)。 診斷:與 outboundRtp 比對——源頭 1080p 但送出 720p = 編碼/傳輸降質,非來源問題。 來源:LocalVideoTrack 的 capturer.output sink 讀 CVPixelBufferGetWidth/Height(實測值)。
```swift
let width: Int
```
---
# add(videoRenderer:)
[RevueSDK](../../index.md) › [VideoTrack](../index.md) › add(videoRenderer:)
*Instance Method*
```swift
func add(videoRenderer: VideoRenderer)
```
---
# VideoTrack
[RevueSDK](../index.md) › VideoTrack
*Class*
視訊 Track 中間類別 — 統一管理 VideoRenderer 分發
```swift
class VideoTrack
```
## Overview
LocalVideoTrack 和 RemoteVideoTrack 繼承此類別, 不再各自維護 renderer 清單。
## Topics
### Instance Methods
- [`func add(videoRenderer: VideoRenderer)`](add%28videorenderer:%29/index.md)
- [`func remove(videoRenderer: VideoRenderer)`](remove%28videorenderer:%29/index.md)
## Relationships
### Inherits From
- [`Track`](../track/index.md)
### Inherited By
- [`RemoteVideoTrack`](../remotevideotrack/index.md)
- [`LocalVideoTrack`](../localvideotrack/index.md)
---
# remove(videoRenderer:)
[RevueSDK](../../index.md) › [VideoTrack](../index.md) › remove(videoRenderer:)
*Instance Method*
```swift
func remove(videoRenderer: VideoRenderer)
```
---
# contentMode
[RevueSDK](../../index.md) › [VideoTrackPositionInfo](../index.md) › contentMode
*Instance Property*
內容縮放模式(預設 .fill)
```swift
let contentMode: VideoContentMode
```
---
# VideoTrackPositionInfo
[RevueSDK](../index.md) › VideoTrackPositionInfo
*Structure*
單一 Track 的位置資訊
```swift
struct VideoTrackPositionInfo
```
## Topics
### Initializers
- [`init(track: Track, normalizedRect: CGRect, contentMode: VideoContentMode)`](init%28track:normalizedrect:contentmode:%29/index.md)
### Instance Properties
- [`let contentMode: VideoContentMode`](contentmode/index.md)
內容縮放模式(預設 .fill)
- [`let normalizedRect: CGRect`](normalizedrect/index.md)
正規化座標(0.0~1.0,相對於 VideoTrackCompositor.resolutionSize) x=0, y=0 為左上角,x=1, y=1 為右下角
- [`let track: Track`](track/index.md)
要繪製的 VideoTrack(相機/遠端/BufferTrack 皆可,不需要 publish)
---
# init(track:normalizedRect:contentMode:)
[RevueSDK](../../index.md) › [VideoTrackPositionInfo](../index.md) › init(track:normalizedRect:contentMode:)
*Initializer*
```swift
init(track: Track, normalizedRect: CGRect, contentMode: VideoContentMode = .fill)
```
---
# normalizedRect
[RevueSDK](../../index.md) › [VideoTrackPositionInfo](../index.md) › normalizedRect
*Instance Property*
正規化座標(0.0~1.0,相對於 VideoTrackCompositor.resolutionSize) x=0, y=0 為左上角,x=1, y=1 為右下角
```swift
let normalizedRect: CGRect
```
---
# track
[RevueSDK](../../index.md) › [VideoTrackPositionInfo](../index.md) › track
*Instance Property*
要繪製的 VideoTrack(相機/遠端/BufferTrack 皆可,不需要 publish)
```swift
let track: Track
```
---
# !=(_:_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.AdaptiveStreamPixelDensity](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# VideoView.AdaptiveStreamPixelDensity.auto
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.AdaptiveStreamPixelDensity](../index.md) › VideoView.AdaptiveStreamPixelDensity.auto
*Case*
使用螢幕自身的 scale(`UIScreen.scale`)—— 預設值
```swift
case auto
```
---
# Equatable Implementations
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.AdaptiveStreamPixelDensity](../index.md) › Equatable Implementations
*Article*
---
# VideoView.AdaptiveStreamPixelDensity.fixed(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.AdaptiveStreamPixelDensity](../index.md) › VideoView.AdaptiveStreamPixelDensity.fixed(_:)
*Case*
固定倍率(上限 `maxDensity`)
```swift
case fixed(Double)
```
---
# VideoView.AdaptiveStreamPixelDensity
[RevueSDK](../../index.md) › [VideoView](../index.md) › VideoView.AdaptiveStreamPixelDensity
*Enumeration*
把 view 的邏輯尺寸(point)換算成實體像素,避免 retina 螢幕上要到過小的畫質層。
```swift
enum AdaptiveStreamPixelDensity
```
## Overview
> **TODO**
>
> 尚未實作 —— 詳見 `isAdaptiveStreamEnabled` 的架構前提說明。
## Topics
### Enumeration Cases
- [`case auto`](auto/index.md)
使用螢幕自身的 scale(`UIScreen.scale`)—— 預設值
- [`case fixed(Double)`](fixed%28_:%29/index.md)
固定倍率(上限 `maxDensity`)
### Type Properties
- [`static let maxDensity: Double`](maxdensity/index.md)
密度上限 —— 避免要求過高解析度而吃掉頻寬
---
# maxDensity
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.AdaptiveStreamPixelDensity](../index.md) › maxDensity
*Type Property*
密度上限 —— 避免要求過高解析度而吃掉頻寬
```swift
static let maxDensity: Double
```
---
# adaptiveStreamPixelDensity
[RevueSDK](../../index.md) › [VideoView](../index.md) › adaptiveStreamPixelDensity
*Instance Property*
```swift
@MainActor var adaptiveStreamPixelDensity: AdaptiveStreamPixelDensity
```
## Discussion
> **TODO**
>
> 尚未實作 —— 詳見 `isAdaptiveStreamEnabled`。
---
# adaptiveStreamSize
[RevueSDK](../../index.md) › [VideoView](../index.md) › adaptiveStreamSize
*Instance Property*
回傳實體像素尺寸(邏輯尺寸 × `adaptiveStreamPixelDensity`);尺寸未知時回 `.zero`。
```swift
@MainActor var adaptiveStreamSize: CGSize { get }
```
## Discussion
> **TODO**
>
> 尚未實作 —— 值可正常計算,但無消費端。詳見 `isAdaptiveStreamEnabled`。
---
# add(delegate:)
[RevueSDK](../../index.md) › [VideoView](../index.md) › add(delegate:)
*Instance Method*
```swift
@MainActor func add(delegate: VideoViewDelegate)
```
---
# avSampleBufferDisplayLayer
[RevueSDK](../../index.md) › [VideoView](../index.md) › avSampleBufferDisplayLayer
*Instance Property*
取用內部的 `AVSampleBufferDisplayLayer`(僅 SampleBuffer 模式可用)。
```swift
@MainActor var avSampleBufferDisplayLayer: AVSampleBufferDisplayLayer? { get }
```
## Discussion
用於 iOS 畫中畫 —— `AVPictureInPictureController` 僅支援此類 layer、不支援 Metal。
renderer 常駐(`setupRenderLayer` 一次建好、靠 `isHidden` 切換),故 Metal 模式下 該 layer 仍存在、只是**沒有畫面在跑**(`renderWithSampleBuffer` 不被呼叫)。 回傳 `nil` 而非空白 layer,避免 PiP 開起來是黑畫面。
使用前需先設 `renderMode = .sampleBuffer`:
```swift
videoView.renderMode = .sampleBuffer
guard let layer = videoView.avSampleBufferDisplayLayer else { return }
```
建議於 Main Thread 存取。
---
# delegates
[RevueSDK](../../index.md) › [VideoView](../index.md) › delegates
*Instance Property*
事件代理 — 訂閱 `isRendering`、尺寸變化(見 `VideoViewDelegate`)
```swift
@MainActor let delegates: MulticastDelegate
```
---
# didRenderFirstFrame
[RevueSDK](../../index.md) › [VideoView](../index.md) › didRenderFirstFrame
*Instance Property*
首幀是否已渲染 —— 「曾 render 過至少一幀」(單調,換 track 才 reset)
```swift
@MainActor var didRenderFirstFrame: Bool { get }
```
---
# VideoView
[RevueSDK](../index.md) › VideoView
*Class*
視訊顯示元件 — UIView 容器,支援 Metal / SampleBuffer 雙模式渲染
```swift
@MainActor class VideoView
```
## Overview
使用方式:
```swift
let videoView = VideoView(frame: .zero)
videoView.renderMode = .auto // 優先 Metal
videoView.track = someVideoTrack // 自動綁定渲染(weak,track 要由別人持有)
```
## Topics
### Structures
- [`struct PinchToZoomOptions`](pinchtozoomoptions-swift.struct/index.md)
調整的是**相機硬體變焦**(`AVCaptureDevice.videoZoomFactor`),非畫面 transform 放大 —— 故僅對本地相機 track(`LocalVideoTrack` + `CameraCapturer`)有效。
### Initializers
- [`init?(coder: NSCoder)`](init%28coder:%29/index.md)
- [`init(frame: CGRect)`](init%28frame:%29/index.md)
### Instance Properties
- [`var adaptiveStreamPixelDensity: AdaptiveStreamPixelDensity`](adaptivestreampixeldensity-swift.property/index.md)
- [`var adaptiveStreamSize: CGSize`](adaptivestreamsize/index.md)
回傳實體像素尺寸(邏輯尺寸 × `adaptiveStreamPixelDensity`);尺寸未知時回 `.zero`。
- [`var avSampleBufferDisplayLayer: AVSampleBufferDisplayLayer?`](avsamplebufferdisplaylayer/index.md)
取用內部的 `AVSampleBufferDisplayLayer`(僅 SampleBuffer 模式可用)。
- [`let delegates: MulticastDelegate`](delegates/index.md)
事件代理 — 訂閱 `isRendering`、尺寸變化(見 `VideoViewDelegate`)
- [`var didRenderFirstFrame: Bool`](didrenderfirstframe/index.md)
首幀是否已渲染 —— 「曾 render 過至少一幀」(單調,換 track 才 reset)
- [`var isAdaptiveStreamEnabled: Bool`](isadaptivestreamenabled/index.md)
目前僅回報「這個 view 現在看得見嗎」(已佈局 + 未隱藏 + 已啟用),**不觸發任何畫質調整**。
- [`var isAutoZoomResetEnabled: Bool`](isautozoomresetenabled/index.md)
- [`var isDebugMode: Bool`](isdebugmode/index.md)
顯示 track sid、解析度(最近一幀的寬高)、首幀、渲染狀態、renderMode、renderer 數量、FPS; 外框正在渲染(`isRendering`)時綠色、沒在渲染時紅色。開啟時每秒更新一次 FPS,關閉時移除疊加層。 只在 main 讀寫。
- [`var isEnabled: Bool`](isenabled/index.md)
是否啟用渲染
- [`var isFrozen: Bool`](isfrozen/index.md)
是否凍結畫面 —— 停在最後一幀,新幀在 `render()` 入口被丟棄。
- [`var isHidden: Bool`](ishidden/index.md)
是否隱藏 —— 覆寫 `UIView.isHidden`,讓 SDK 感知並連動停止渲染。
- [`var isPinchToZoomEnabled: Bool`](ispinchtozoomenabled/index.md)
設 `true` 只加入 `.zoomIn`(只開放放大);設 `false` 只拿掉 `.zoomIn`。 讀取時回傳 `pinchToZoomOptions.isEnabled`(還有 `.zoomOut` 時仍是 true)。
- [`var isRendering: Bool`](isrendering/index.md)
是否正在渲染 —— 「這道 track 目前有沒有真的在出畫面」
- [`var layoutMode: LayoutMode`](layoutmode-swift.property/index.md)
佈局模式
- [`var mirrorMode: MirrorMode`](mirrormode-swift.property/index.md)
鏡像模式
- [`var pinchToZoomOptions: PinchToZoomOptions`](pinchtozoomoptions-swift.property/index.md)
有 `zoomIn` 或 `zoomOut` 時雙指手勢才啟用,空的就停用。改值時把鏡頭目前倍率夾回新的允許範圍 (含 `resetOnRelease` 時直接回預設倍率)。只在 main 讀寫。
- [`var renderMode: RenderMode`](rendermode-swift.property/index.md)
渲染模式
- [`var rotationOverride: VideoRotation?`](rotationoverride/index.md)
收到的畫面已經是正的(發送端轉正),這只是 App 額外指定的顯示角度。 填法(`layoutMode`)以轉向後的寬高計算;和鏡像合成同一個 transform(先轉再鏡像)。
- [`var track: VideoTrack?`](track/index.md)
綁定的 Track — 設定後自動開始/停止渲染
### Instance Methods
- [`func add(delegate: VideoViewDelegate)`](add%28delegate:%29/index.md)
- [`func layoutSubviews()`](layoutsubviews%28%29/index.md)
- [`func remove(delegate: VideoViewDelegate)`](remove%28delegate:%29/index.md)
移除代理 —— 轉呼 `delegates.remove(delegate:)`
- [`func removeAllDelegates()`](removealldelegates%28%29/index.md)
移除所有代理 —— 轉呼 `delegates.removeAll()`
- [`func set(size: CGSize)`](set%28size:%29/index.md)
本專案 renderer 尺寸由 `layoutSubviews` 直接同步,此方法目前為 no-op。 詳見 `isAdaptiveStreamEnabled`。
### Type Methods
- [`static func isMetalAvailable() -> Bool`](ismetalavailable%28%29/index.md)
### Enumerations
- [`enum AdaptiveStreamPixelDensity`](adaptivestreampixeldensity-swift.enum/index.md)
把 view 的邏輯尺寸(point)換算成實體像素,避免 retina 螢幕上要到過小的畫質層。
- [`enum LayoutMode`](layoutmode-swift.enum/index.md)
佈局模式
- [`enum MirrorMode`](mirrormode-swift.enum/index.md)
鏡像模式
- [`enum RenderMode`](rendermode-swift.enum/index.md)
渲染模式
## Relationships
### Conforms To
- [`VideoRenderer`](../videorenderer/index.md)
---
# init(coder:)
[RevueSDK](../../index.md) › [VideoView](../index.md) › init(coder:)
*Initializer*
```swift
@MainActor required init?(coder: NSCoder)
```
---
# init(frame:)
[RevueSDK](../../index.md) › [VideoView](../index.md) › init(frame:)
*Initializer*
```swift
@MainActor override init(frame: CGRect)
```
---
# isAdaptiveStreamEnabled
[RevueSDK](../../index.md) › [VideoView](../index.md) › isAdaptiveStreamEnabled
*Instance Property*
目前僅回報「這個 view 現在看得見嗎」(已佈局 + 未隱藏 + 已啟用),**不觸發任何畫質調整**。
```swift
@MainActor var isAdaptiveStreamEnabled: Bool { get }
```
## Discussion
SFU —— 客戶端回報 view 尺寸給伺服器,伺服器據此挑選 simulcast 層。本專案為 **P2P**,
若要達成同等效果,需改走「接收端請發送端調整編碼參數」的跨端協商: 透過好友訊息層(類似會議室 mute 廣播機制)送出目標解析度,由發送端調整 `CameraCaptureOptions`。此為跨 iOS / Android / Core 的協定設計,範圍遠大於 `VideoView`, 需另立議題。在該協定完成前,本群 API(`isAdaptiveStreamEnabled` / `adaptiveStreamSize` / `adaptiveStreamPixelDensity` / `set(size:)`)僅為介面佔位。
---
# isAutoZoomResetEnabled
[RevueSDK](../../index.md) › [VideoView](../index.md) › isAutoZoomResetEnabled
*Instance Property*
```swift
@MainActor var isAutoZoomResetEnabled: Bool { get set }
```
---
# isDebugMode
[RevueSDK](../../index.md) › [VideoView](../index.md) › isDebugMode
*Instance Property*
顯示 track sid、解析度(最近一幀的寬高)、首幀、渲染狀態、renderMode、renderer 數量、FPS; 外框正在渲染(`isRendering`)時綠色、沒在渲染時紅色。開啟時每秒更新一次 FPS,關閉時移除疊加層。 只在 main 讀寫。
```swift
@MainActor var isDebugMode: Bool { get set }
```
---
# isEnabled
[RevueSDK](../../index.md) › [VideoView](../index.md) › isEnabled
*Instance Property*
是否啟用渲染
```swift
@MainActor var isEnabled: Bool { get set }
```
## Discussion
設 `false` 暫停渲染(幀在 `render()` 入口被丟棄 + 清畫面),適合 `UICollectionViewCell` 重用等「view 還在但使用者看不到」的場景。與 `isHidden` 共同決定 `shouldRender`。
---
# isFrozen
[RevueSDK](../../index.md) › [VideoView](../index.md) › isFrozen
*Instance Property*
是否凍結畫面 —— 停在最後一幀,新幀在 `render()` 入口被丟棄。
```swift
@MainActor var isFrozen: Bool
```
## Discussion
與 `isEnabled = false` 的差別:後者清空畫面(黑屏),凍結則**保留最後一幀**。 資料流不受影響 —— 幀持續接收,只是不繪製。
停格原理:`VideoMetalView` 是手動觸發模式(`isPaused = true`), 不設 `currentCIImage` 就不觸發 `draw()`,framebuffer 維持上次結果、GPU 不做功。
兩個已知行為(設計決定,非 bug):
- 凍結期間 `renderDate` 停止更新 → 2.0s 後 `isRendering` 轉 `false` (`isRendering` 語意為「畫面是不是活的」,凍結時確實不是)
- 換 track 或 SDK 清畫面會清掉停格畫面,但**凍結設定保留** —— 此時螢幕為黑且新幀仍被擋住,需自行設回 `false` (`isFrozen` 屬 view 顯示設定,與 `layoutMode` / `mirrorMode` 同族,不隨 track 重置)
---
# isHidden
[RevueSDK](../../index.md) › [VideoView](../index.md) › isHidden
*Instance Property*
是否隱藏 —— 覆寫 `UIView.isHidden`,讓 SDK 感知並連動停止渲染。
```swift
@MainActor override var isHidden: Bool { get set }
```
## Discussion
額外參與 `shouldRender` 判定 —— 藏起來就不再花 GPU 繪製看不見的畫面。
---
# isMetalAvailable()
[RevueSDK](../../index.md) › [VideoView](../index.md) › isMetalAvailable()
*Type Method*
```swift
@MainActor static func isMetalAvailable() -> Bool
```
---
# isPinchToZoomEnabled
[RevueSDK](../../index.md) › [VideoView](../index.md) › isPinchToZoomEnabled
*Instance Property*
設 `true` 只加入 `.zoomIn`(只開放放大);設 `false` 只拿掉 `.zoomIn`。 讀取時回傳 `pinchToZoomOptions.isEnabled`(還有 `.zoomOut` 時仍是 true)。
```swift
@MainActor var isPinchToZoomEnabled: Bool { get set }
```
---
# isRendering
[RevueSDK](../../index.md) › [VideoView](../index.md) › isRendering
*Instance Property*
是否正在渲染 —— 「這道 track 目前有沒有真的在出畫面」
```swift
@MainActor var isRendering: Bool { get set }
```
## Discussion
首幀 render 時設 `true`;畫面凍結超過 `Self.freezeDetectThreshold`(2.0s)掉回 `false`。 變化時通知 `delegates`(Main Thread)。
---
# !=(_:_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.LayoutMode](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.LayoutMode](../index.md) › Equatable Implementations
*Article*
---
# VideoView.LayoutMode.fill
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.LayoutMode](../index.md) › VideoView.LayoutMode.fill
*Case*
填滿(裁切,不留黑邊)
```swift
case fill
```
---
# VideoView.LayoutMode.fit
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.LayoutMode](../index.md) › VideoView.LayoutMode.fit
*Case*
適應(完整顯示,可能留黑邊)
```swift
case fit
```
---
# hash(into:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.LayoutMode](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.LayoutMode](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# VideoView.LayoutMode
[RevueSDK](../../index.md) › [VideoView](../index.md) › VideoView.LayoutMode
*Enumeration*
佈局模式
```swift
enum LayoutMode
```
## Topics
### Enumeration Cases
- [`case fill`](fill/index.md)
填滿(裁切,不留黑邊)
- [`case fit`](fit/index.md)
適應(完整顯示,可能留黑邊)
### Initializers
- [`init?(rawValue: Int)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.LayoutMode](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.LayoutMode](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.LayoutMode](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.LayoutMode](../index.md) › RawRepresentable Implementations
*Article*
---
# layoutMode
[RevueSDK](../../index.md) › [VideoView](../index.md) › layoutMode
*Instance Property*
佈局模式
```swift
@MainActor var layoutMode: LayoutMode { get set }
```
---
# layoutSubviews()
[RevueSDK](../../index.md) › [VideoView](../index.md) › layoutSubviews()
*Instance Method*
```swift
@MainActor override func layoutSubviews()
```
---
# !=(_:_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.MirrorMode](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# VideoView.MirrorMode.auto
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.MirrorMode](../index.md) › VideoView.MirrorMode.auto
*Case*
自動(前鏡頭鏡像,後鏡頭不鏡像)
```swift
case auto
```
---
# Equatable Implementations
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.MirrorMode](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.MirrorMode](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.MirrorMode](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# VideoView.MirrorMode
[RevueSDK](../../index.md) › [VideoView](../index.md) › VideoView.MirrorMode
*Enumeration*
鏡像模式
```swift
enum MirrorMode
```
## Topics
### Enumeration Cases
- [`case auto`](auto/index.md)
自動(前鏡頭鏡像,後鏡頭不鏡像)
- [`case mirror`](mirror/index.md)
永遠鏡像
- [`case off`](off/index.md)
不鏡像
### Initializers
- [`init?(rawValue: Int)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.MirrorMode](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.MirrorMode](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# VideoView.MirrorMode.mirror
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.MirrorMode](../index.md) › VideoView.MirrorMode.mirror
*Case*
永遠鏡像
```swift
case mirror
```
---
# VideoView.MirrorMode.off
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.MirrorMode](../index.md) › VideoView.MirrorMode.off
*Case*
不鏡像
```swift
case off
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.MirrorMode](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.MirrorMode](../index.md) › RawRepresentable Implementations
*Article*
---
# mirrorMode
[RevueSDK](../../index.md) › [VideoView](../index.md) › mirrorMode
*Instance Property*
鏡像模式
```swift
@MainActor var mirrorMode: MirrorMode { get set }
```
---
# pinchToZoomOptions
[RevueSDK](../../index.md) › [VideoView](../index.md) › pinchToZoomOptions
*Instance Property*
有 `zoomIn` 或 `zoomOut` 時雙指手勢才啟用,空的就停用。改值時把鏡頭目前倍率夾回新的允許範圍 (含 `resetOnRelease` 時直接回預設倍率)。只在 main 讀寫。
```swift
@MainActor var pinchToZoomOptions: PinchToZoomOptions { get set }
```
---
# !=(_:_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# contains(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › contains(_:)
*Instance Method*
```swift
func contains(_ member: Self) -> Bool
```
---
# Equatable Implementations
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › Equatable Implementations
*Article*
---
# formIntersection(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › formIntersection(_:)
*Instance Method*
```swift
mutating func formIntersection(_ other: Self)
```
---
# formSymmetricDifference(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › formSymmetricDifference(_:)
*Instance Method*
```swift
mutating func formSymmetricDifference(_ other: Self)
```
---
# formUnion(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › formUnion(_:)
*Instance Method*
```swift
mutating func formUnion(_ other: Self)
```
---
# VideoView.PinchToZoomOptions
[RevueSDK](../../index.md) › [VideoView](../index.md) › VideoView.PinchToZoomOptions
*Structure*
調整的是**相機硬體變焦**(`AVCaptureDevice.videoZoomFactor`),非畫面 transform 放大 —— 故僅對本地相機 track(`LocalVideoTrack` + `CameraCapturer`)有效。
```swift
struct PinchToZoomOptions
```
## Overview
遠端畫面的 track 是 `RemoteVideoTrack`,取不到 `AVCaptureDevice`(那是對方裝置的 若要「遠端畫面也能放大看」,需另做畫面 transform 縮放(會失真,且需處理平移與 `layoutMode` / `mirrorMode` 既有 transform 的疊加),不在本 API 範圍。
`.builtInTripleCamera`、`.builtInDualWideCamera` 是 2(這兩種的 1 倍是超廣角),其他是 1。
## Topics
### Initializers
- [`init(rawValue: UInt)`](init%28rawvalue:%29/index.md)
### Instance Properties
- [`var isEnabled: Bool`](isenabled/index.md)
是否啟用(`zoomIn` / `zoomOut` 任一存在即為啟用)
- [`let rawValue: UInt`](rawvalue/index.md)
### Type Properties
- [`static let resetOnRelease: VideoView.PinchToZoomOptions`](resetonrelease/index.md)
放開手指後自動回復預設倍率
- [`static let zoomIn: VideoView.PinchToZoomOptions`](zoomin/index.md)
允許放大超過裝置預設倍率
- [`static let zoomOut: VideoView.PinchToZoomOptions`](zoomout/index.md)
允許縮小超過裝置預設倍率
---
# init()
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › init()
*Initializer*
```swift
init()
```
---
# init(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › init(_:)
*Initializer*
```swift
init(_ sequence: S) where S : Sequence, Self.Element == S.Element
```
---
# init(arrayLiteral:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › init(arrayLiteral:)
*Initializer*
```swift
init(arrayLiteral: Self.Element...)
```
---
# init(rawValue:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › init(rawValue:)
*Initializer*
```swift
init(rawValue: UInt)
```
---
# insert(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › insert(_:)
*Instance Method*
```swift
@discardableResult mutating func insert(_ newMember: Self.Element) -> (inserted: Bool, memberAfterInsert: Self.Element)
```
---
# insert(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › insert(_:)
*Instance Method*
```swift
@discardableResult mutating func insert(_ newMember: Self.Element) -> (inserted: Bool, memberAfterInsert: Self.Element)
```
---
# intersection(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › intersection(_:)
*Instance Method*
```swift
func intersection(_ other: Self) -> Self
```
---
# isDisjoint(with:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › isDisjoint(with:)
*Instance Method*
```swift
func isDisjoint(with other: Self) -> Bool
```
---
# isEmpty
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › isEmpty
*Instance Property*
```swift
var isEmpty: Bool { get }
```
---
# isEnabled
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › isEnabled
*Instance Property*
是否啟用(`zoomIn` / `zoomOut` 任一存在即為啟用)
```swift
var isEnabled: Bool { get }
```
---
# isStrictSubset(of:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › isStrictSubset(of:)
*Instance Method*
```swift
func isStrictSubset(of other: Self) -> Bool
```
---
# isStrictSuperset(of:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › isStrictSuperset(of:)
*Instance Method*
```swift
func isStrictSuperset(of other: Self) -> Bool
```
---
# isSubset(of:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › isSubset(of:)
*Instance Method*
```swift
func isSubset(of other: Self) -> Bool
```
---
# isSuperset(of:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › isSuperset(of:)
*Instance Method*
```swift
func isSuperset(of other: Self) -> Bool
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# OptionSet Implementations
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › OptionSet Implementations
*Article*
---
# rawAttachmentValueRepresentation
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › RawRepresentable Implementations
*Article*
---
# rawValue
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › rawValue
*Instance Property*
```swift
let rawValue: UInt
```
---
# remove(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › remove(_:)
*Instance Method*
```swift
@discardableResult mutating func remove(_ member: Self.Element) -> Self.Element?
```
---
# resetOnRelease
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › resetOnRelease
*Type Property*
放開手指後自動回復預設倍率
```swift
static let resetOnRelease: VideoView.PinchToZoomOptions
```
---
# SetAlgebra Implementations
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › SetAlgebra Implementations
*Article*
---
# subtract(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › subtract(_:)
*Instance Method*
```swift
mutating func subtract(_ other: Self)
```
---
# subtracting(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › subtracting(_:)
*Instance Method*
```swift
func subtracting(_ other: Self) -> Self
```
---
# symmetricDifference(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › symmetricDifference(_:)
*Instance Method*
```swift
func symmetricDifference(_ other: Self) -> Self
```
---
# union(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › union(_:)
*Instance Method*
```swift
func union(_ other: Self) -> Self
```
---
# update(with:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › update(with:)
*Instance Method*
```swift
@discardableResult mutating func update(with newMember: Self.Element) -> Self.Element?
```
---
# zoomIn
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › zoomIn
*Type Property*
允許放大超過裝置預設倍率
```swift
static let zoomIn: VideoView.PinchToZoomOptions
```
---
# zoomOut
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.PinchToZoomOptions](../index.md) › zoomOut
*Type Property*
允許縮小超過裝置預設倍率
```swift
static let zoomOut: VideoView.PinchToZoomOptions
```
---
# remove(delegate:)
[RevueSDK](../../index.md) › [VideoView](../index.md) › remove(delegate:)
*Instance Method*
移除代理 —— 轉呼 `delegates.remove(delegate:)`
```swift
@MainActor func remove(delegate: VideoViewDelegate)
```
---
# removeAllDelegates()
[RevueSDK](../../index.md) › [VideoView](../index.md) › removeAllDelegates()
*Instance Method*
移除所有代理 —— 轉呼 `delegates.removeAll()`
```swift
@MainActor func removeAllDelegates()
```
---
# render(pixelBuffer:)
[RevueSDK](../../index.md) › [VideoView](../index.md) › render(pixelBuffer:)
*Instance Method*
```swift
@MainActor func render(pixelBuffer: CVPixelBuffer)
```
---
# !=(_:_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.RenderMode](../index.md) › !=(_:_:)
*Operator*
```swift
static func != (lhs: borrowing Self, rhs: borrowing Self) -> Bool
```
---
# VideoView.RenderMode.auto
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.RenderMode](../index.md) › VideoView.RenderMode.auto
*Case*
自動選擇(優先 Metal,不可用時 SampleBuffer)
```swift
case auto
```
---
# Equatable Implementations
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.RenderMode](../index.md) › Equatable Implementations
*Article*
---
# hash(into:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.RenderMode](../index.md) › hash(into:)
*Instance Method*
```swift
func hash(into hasher: inout Hasher)
```
---
# hashValue
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.RenderMode](../index.md) › hashValue
*Instance Property*
```swift
var hashValue: Int { get }
```
---
# VideoView.RenderMode
[RevueSDK](../../index.md) › [VideoView](../index.md) › VideoView.RenderMode
*Enumeration*
渲染模式
```swift
enum RenderMode
```
## Topics
### Enumeration Cases
- [`case auto`](auto/index.md)
自動選擇(優先 Metal,不可用時 SampleBuffer)
- [`case metal`](metal/index.md)
Metal 渲染(不可用時 fallback SampleBuffer)
- [`case sampleBuffer`](samplebuffer/index.md)
AVSampleBufferDisplayLayer
### Initializers
- [`init?(rawValue: Int)`](init%28rawvalue:%29/index.md)
---
# init(rawValue:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.RenderMode](../index.md) › init(rawValue:)
*Initializer*
```swift
init?(rawValue: Int)
```
---
# makeFromRawAttachmentValue(_:)
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.RenderMode](../index.md) › makeFromRawAttachmentValue(_:)
*Type Method*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
static func makeFromRawAttachmentValue(_ repr: CVAttachmentRawValue) -> Self?
```
---
# VideoView.RenderMode.metal
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.RenderMode](../index.md) › VideoView.RenderMode.metal
*Case*
Metal 渲染(不可用時 fallback SampleBuffer)
```swift
case metal
```
---
# rawAttachmentValueRepresentation
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.RenderMode](../index.md) › rawAttachmentValueRepresentation
*Instance Property*
- iOS 27.0+
- iPadOS 27.0+
- macCatalyst 27.0+
- macOS 27.0+
- tvOS 27.0+
- visionOS 27.0+
- watchOS 27.0+
```swift
var rawAttachmentValueRepresentation: CVAttachmentRawValue { get }
```
---
# RawRepresentable Implementations
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.RenderMode](../index.md) › RawRepresentable Implementations
*Article*
---
# VideoView.RenderMode.sampleBuffer
[RevueSDK](../../../index.md) › [VideoView](../../index.md) › [VideoView.RenderMode](../index.md) › VideoView.RenderMode.sampleBuffer
*Case*
AVSampleBufferDisplayLayer
```swift
case sampleBuffer
```
---
# renderMode
[RevueSDK](../../index.md) › [VideoView](../index.md) › renderMode
*Instance Property*
渲染模式
```swift
@MainActor var renderMode: RenderMode { get set }
```
---
# rotationOverride
[RevueSDK](../../index.md) › [VideoView](../index.md) › rotationOverride
*Instance Property*
收到的畫面已經是正的(發送端轉正),這只是 App 額外指定的顯示角度。 填法(`layoutMode`)以轉向後的寬高計算;和鏡像合成同一個 transform(先轉再鏡像)。
```swift
@MainActor var rotationOverride: VideoRotation? { get set }
```
## Discussion
- Metal:畫之前把圖片轉向,下一幀起生效。
- SampleBuffer:轉整個 layer,90°/270° 時 layer 寬高對調,設值當下生效。
App 在 main 寫;Metal 路徑在送幀的執行緒讀,所以存在鎖裡。
---
# set(size:)
[RevueSDK](../../index.md) › [VideoView](../index.md) › set(size:)
*Instance Method*
本專案 renderer 尺寸由 `layoutSubviews` 直接同步,此方法目前為 no-op。 詳見 `isAdaptiveStreamEnabled`。
```swift
@MainActor func set(size: CGSize)
```
---
# track
[RevueSDK](../../index.md) › [VideoView](../index.md) › track
*Instance Property*
綁定的 Track — 設定後自動開始/停止渲染
```swift
@MainActor weak var track: VideoTrack? { get set }
```
## Discussion
遠端 track 由 publication 持有;本地 `cameraTrack` 等由 `LocalParticipant` 持有到 `Revue.destroy()`;App 自己建的 track(如 `createBufferTrack`)要自己持有。 track 被釋放時這裡自動變 nil 並清空畫面(見 `trackDidRelease()`)。只在 main 讀寫。
---
# VideoRenderer Implementations
[RevueSDK](../../index.md) › [VideoView](../index.md) › VideoRenderer Implementations
*Article*
---
# VideoViewDelegate
[RevueSDK](../index.md) › VideoViewDelegate
*Protocol*
視訊顯示元件事件代理 — 訂閱單一 `VideoView` 的渲染狀態變化
```swift
protocol VideoViewDelegate : AnyObject
```
## Overview
**主要用途 — 空殼 track 佔位**:媒體握手改為 declare 驅動後,接收端「收到 declare 就建 track」,但第一幀畫面通常還沒到(尤其 relay 連線慢),產生「空殼 track」(有 track 物件、 無畫面)。這是握手制的必然中間態。UI 可訂閱本代理,依 `isRendering` 決定顯示佔位 / loading。
使用方式:
```swift
videoView.add(delegate: self)
func videoView(_ videoView: VideoView, didUpdate isRendering: Bool) {
placeholderView.isHidden = isRendering // 有畫面才隱藏佔位
}
```
所有方法都有預設空實作,可選擇性實作需要的事件。 通知一律在 Main Thread 觸發(UI 相關)。
## Topics
### Instance Methods
- [`func videoView(VideoView, didUpdate: Bool)`](videoview%28_:didupdate:%29-2zacq/index.md)
`VideoView.isRendering` 已更新
- [`func videoView(VideoView, didUpdate: CGSize)`](videoview%28_:didupdate:%29-887oi/index.md)
`VideoView` 自己的大小已改變(`bounds.size`;只移動位置不通知)
---
# videoView(_:didUpdate:)
[RevueSDK](../../index.md) › [VideoViewDelegate](../index.md) › videoView(_:didUpdate:)
*Instance Method*
```swift
func videoView(_ videoView: VideoView, didUpdate isRendering: Bool)
```
---
# videoView(_:didUpdate:)
[RevueSDK](../../index.md) › [VideoViewDelegate](../index.md) › videoView(_:didUpdate:)
*Instance Method*
`VideoView.isRendering` 已更新
```swift
func videoView(_ videoView: VideoView, didUpdate isRendering: Bool)
```
## Discussion
- `true`:首幀已 render(或凍結後恢復出幀)
- `false`:畫面凍結超過 2.0s、SDK 清畫面(例如本地 mute 又沒有佔位畫面)、換 track、 或主動凍結(`VideoView.isFrozen = true`,2.0s 後轉 `false`)
> **Note**
>
> 本值語意為「**畫面是不是活的**」而非「幀有沒有在來」。 主動凍結期間對方畫面仍持續送達,但本值會是 `false` —— 上層若用本值判斷連線狀態,須自行排除主動凍結情境。
---
# videoView(_:didUpdate:)
[RevueSDK](../../index.md) › [VideoViewDelegate](../index.md) › videoView(_:didUpdate:)
*Instance Method*
```swift
func videoView(_ videoView: VideoView, didUpdate size: CGSize)
```
---
# videoView(_:didUpdate:)
[RevueSDK](../../index.md) › [VideoViewDelegate](../index.md) › videoView(_:didUpdate:)
*Instance Method*
`VideoView` 自己的大小已改變(`bounds.size`;只移動位置不通知)
```swift
func videoView(_ videoView: VideoView, didUpdate size: CGSize)
```
## Discussion
在 `layoutSubviews` 偵測,帶新的尺寸(point,未乘螢幕 scale)。
---
# allKeyframe
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › allKeyframe
*Type Property*
全 keyframe(每幀完整、不破圖)— init default
```swift
static let allKeyframe: Vp8Encoding
```
## Discussion
bitrate 3 Mbps / cpuUsed 16 / quantizer 30-40 / kf 1 / VBR
---
# cpuUsed
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › cpuUsed
*Instance Property*
編碼器 CPU 用力程度(-16 ~ 16)
```swift
let cpuUsed: Int8
```
## Discussion
- 越小 → 越用力壓縮、CPU 吃重、畫質好但慢
- 越大 → 越省力、快但畫質下降
- 對應 C 層 `revuekit_av_video_set_cpu_used`。下一幀生效。
---
# cqLevel
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › cqLevel
*Instance Property*
CQ 模式下的目標畫質等級(0 ~ 63)
```swift
let cqLevel: UInt8
```
## Discussion
- 僅 `rateControlMode == .cq` 時生效、其他模式忽略
- 跟 quantizer 同尺度:越小畫質目標越高、越大越省流量
- 對應 C 層 `revuekit_av_video_set_rc_mode` 的第 4 參數
---
# highQuality
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › highQuality
*Type Property*
高畫質 preset(對應 C 層 `apply_high_quality_mode`)
```swift
static let highQuality: Vp8Encoding
```
## Discussion
bitrate 8 Mbps / cpuUsed 4 / quantizer 2-40 / kf 60 / VBR
---
# Vp8Encoding
[RevueSDK](../index.md) › Vp8Encoding
*Structure*
VP8 編碼參數
```swift
struct Vp8Encoding
```
## Overview
8 個欄位對應 revuekit-c 的 8 個 VP8 setter。所有欄位皆 non-optional 且有預設值, `Vp8Encoding()` 等同 `.allKeyframe`(不破圖精神)。
## 4 個 preset 常數
| Preset | maxBitrate | cpuUsed | quantizer | kf | rcMode |
| --- | --- | --- | --- | --- | --- |
| `.allKeyframe` (init 預設) | 3 Mbps | 16 | 30-40 | 1 | VBR |
| `.lowLatency` (C 對應) | 3 Mbps | 12 | 4-48 | 15 | CBR |
| `.highQuality` (C 對應) | 8 Mbps | 4 | 2-40 | 60 | VBR |
| `.lowBandwidth` (C 對應) | 1.5 Mbps | 16 | 10-56 | 10 | CQ |
## Topics
### Initializers
- [`init(maxBitrate: Int, cpuUsed: Int8, quantizerMin: UInt8, quantizerMax: UInt8, keyframeInterval: UInt16, rateControlMode: RcMode, cqLevel: UInt8, temporalLayers: UInt32)`](init%28maxbitrate:cpuused:quantizermin:quantizermax:keyframeinterval:ratecontrolmode:cqlevel:temporallayers:%29/index.md)
### Instance Properties
- [`let cpuUsed: Int8`](cpuused/index.md)
編碼器 CPU 用力程度(-16 ~ 16)
- [`let cqLevel: UInt8`](cqlevel/index.md)
CQ 模式下的目標畫質等級(0 ~ 63)
- [`let keyframeInterval: UInt16`](keyframeinterval/index.md)
Keyframe 間隔(幀數)
- [`let maxBitrate: Int`](maxbitrate/index.md)
目標位元率(bps)
- [`let quantizerMax: UInt8`](quantizermax/index.md)
Quantizer 上限(0 ~ 63)
- [`let quantizerMin: UInt8`](quantizermin/index.md)
Quantizer 下限(0 ~ 63)
- [`let rateControlMode: RcMode`](ratecontrolmode/index.md)
Rate Control 模式
- [`let temporalLayers: UInt32`](temporallayers/index.md)
Temporal Layers 數量(1 ~ 3)
### Type Properties
- [`static let allKeyframe: Vp8Encoding`](allkeyframe/index.md)
全 keyframe(每幀完整、不破圖)— init default
- [`static let highQuality: Vp8Encoding`](highquality/index.md)
高畫質 preset(對應 C 層 `apply_high_quality_mode`)
- [`static let lowBandwidth: Vp8Encoding`](lowbandwidth/index.md)
弱網優先 preset(對應 C 層 `apply_low_bandwidth_mode`)
- [`static let lowLatency: Vp8Encoding`](lowlatency/index.md)
低延遲 preset(對應 C 層 `apply_low_latency_mode`)
---
# init(maxBitrate:cpuUsed:quantizerMin:quantizerMax:keyframeInterval:rateControlMode:cqLevel:temporalLayers:)
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › init(maxBitrate:cpuUsed:quantizerMin:quantizerMax:keyframeInterval:rateControlMode:cqLevel:temporalLayers:)
*Initializer*
```swift
init(maxBitrate: Int = 3_000_000, cpuUsed: Int8 = 16, quantizerMin: UInt8 = 30, quantizerMax: UInt8 = 40, keyframeInterval: UInt16 = 1, rateControlMode: RcMode = .vbr, cqLevel: UInt8 = 30, temporalLayers: UInt32 = 1)
```
---
# keyframeInterval
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › keyframeInterval
*Instance Property*
Keyframe 間隔(幀數)
```swift
let keyframeInterval: UInt16
```
## Discussion
- 每 N 幀送一張 keyframe(完整畫面)
- 越短 → 破圖恢復越快、但頻寬用越多
- 越長 → 越省頻寬、但丟包破圖累積時間長
- `1` = 每幀 keyframe(等同「不破圖」模式,但頻寬大)
- 對應 C 層 `revuekit_av_video_set_keyframe_interval`。**會觸發編碼器重啟**。
---
# lowBandwidth
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › lowBandwidth
*Type Property*
弱網優先 preset(對應 C 層 `apply_low_bandwidth_mode`)
```swift
static let lowBandwidth: Vp8Encoding
```
## Discussion
bitrate 1.5 Mbps / cpuUsed 16 / quantizer 10-56 / kf 10 / CQ
---
# lowLatency
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › lowLatency
*Type Property*
低延遲 preset(對應 C 層 `apply_low_latency_mode`)
```swift
static let lowLatency: Vp8Encoding
```
## Discussion
bitrate 3 Mbps / cpuUsed 12 / quantizer 4-48 / kf 15 / CBR
---
# maxBitrate
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › maxBitrate
*Instance Property*
目標位元率(bps)
```swift
let maxBitrate: Int
```
## Discussion
對應 C 層 `revuekit_av_video_set_bitrate`(內部除 1000 轉 kbps)。下一幀生效。
---
# quantizerMax
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › quantizerMax
*Instance Property*
Quantizer 上限(0 ~ 63)
```swift
let quantizerMax: UInt8
```
## Discussion
壓縮強度區間的「上限」— 值越大、允許壓越狠、畫質可掉更低。 對應 C 層 `revuekit_av_video_set_quantizer_range` 的 `max` 參數。 **必須 `>= quantizerMin`**。
---
# quantizerMin
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › quantizerMin
*Instance Property*
Quantizer 下限(0 ~ 63)
```swift
let quantizerMin: UInt8
```
## Discussion
壓縮強度區間的「下限」— 值越小、畫質下限越高、資料越大。 對應 C 層 `revuekit_av_video_set_quantizer_range` 的 `min` 參數。
---
# rateControlMode
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › rateControlMode
*Instance Property*
Rate Control 模式
```swift
let rateControlMode: RcMode
```
## Discussion
- `.vbr` — Variable Bitrate、畫質為目標、bitrate 浮動
- `.cbr` — Constant Bitrate、bitrate 固定、畫質浮動
- `.cq` — Constrained Quality、畫質為主、bitrate 為輔(弱網最佳)
對應 C 層 `revuekit_av_video_set_rc_mode` 的第 3 參數。**會觸發編碼器重啟**。
---
# temporalLayers
[RevueSDK](../../index.md) › [Vp8Encoding](../index.md) › temporalLayers
*Instance Property*
Temporal Layers 數量(1 ~ 3)
```swift
let temporalLayers: UInt32
```
## Discussion
時間分層編碼 — 把幀依重要性分成 1~3 層、弱網時接收端可丟低優先層。
- `1` = 不分層(預設)
- `2` / `3` = 弱網場景,需要接收端支援選層
- 對應 C 層 `revuekit_av_video_set_temporal_layers`。**會觸發編碼器重啟**。