// # Group Call Protocol
//
// Note that group calls are not necessarily bound to a Threema group. _Group_
// refers to a group of call participants and is a way to distinguish from 1:1
// Threema calls.
//
// There are two primary variants which use the same technology underneath:
//
// - A group call scoped to a (Threema) group is simple and easy to use. It does
//   not have any advanced functionality such as administration or external
//   guests. Only one group call is intended to run within a group.
// - A conference call is a more advanced type of group call and delivers more
//   advanced functionality such as administration. Concrete specification
//   pending.
//
// The theoretical maximum amount of participants is 790 (due to the way we
// derive WebRTC MIDs) but the practical limit is way below that.
//
// ## Terminology
//
// - `GCK`: Group Call Key, only used for key derivation
// - `GCKH`: Group Call Key Hash
// - `GCNHAK`: Group Call Normal Handshake Authentication Key
// - `GCHK`: Group Call Handshake Key
// - `GCSK`: Group Call State Key
// - `GCAK`: Group Call Administrator Key, only used for key derivation
// - `GCAMK`: Group Call Administrator Message Key
// - `PCK`: Participant Call Key
// - `PCMK`: Participant Call Media Key, only used for key derivation
// - `PCMK`': Ratchet iteration of PCMK
// - `PCMFK`: Participant Call Media Frame Key
// - `PCCK`: Participant Call Cookie
// - `PCSN`: Participant Call Sequence Number
// - `MFSN`: Media Frame Sequence Number
//
// ## General Information
//
// **Endianness**: All integers use little-endian encoding.
//
// **Encryption cipher**: XSalsa20-Poly1305, unless otherwise specified.
//
// **Nonce format**:
//
// - a 16 byte cookie (PCCK), followed by
// - a monotonically increasing sequence number (PCSN, u64-le).
//
// **Sequence number**: The sequence number starts with `1` and is counted
// separately for each direction (i.e. there is one sequence number counter for
// the sender and one for the receiver). We will use `PCSN+` in this document to
// denote that the counter should be increased **after** the value has been
// inserted (i.e. semantically equivalent to `x++` in many languages).
//
// Note: This format is equivalent to the CSP transport encryption.
//
// ## Key Derivation
//
// Note: All keys that are not derived from `GCK` directly will be derived using
// `GCKH` as input. This ensures that exchanged secret keys are useless if the
// Group Call ID has been exposed (unless `GCK` is also known to the attacker).
//
//     GCKH = BLAKE2b(key=GCK, salt='#', personal='3ma-call')
//
//     GCHK = BLAKE2b(key=GCK, salt='h', personal='3ma-call')
//     GCSK = BLAKE2b(key=GCK, salt='s', personal='3ma-call')
//
//     GCAMK = BLAKE2b(key=GCAK, salt='am', personal='3ma-call', input=GCKH)
//
//     PCMK' = BLAKE2b(key=PCMK, salt="m'", personal='3ma-call')
//     PCMFK = BLAKE2b(key=PCMK, salt='mf', personal='3ma-call', input=GCKH)
//
// ## Group Call ID Derivation
//
// For group calls scoped to groups, the Group Call ID is derived by running
// BLAKE2b on specific data provided by the `GroupCallStart`:
//
//     group-call-id = BLAKE2b(
//       out-length=32,
//       salt='i',
//       personal='3ma-call',
//       input=
//            group-creator-identity
//         || group-id
//         || u8(GroupCallStart.protocol_version)
//         || GroupCallStart.gck
//         || utf8-encode(GroupCallStart.sfu_base_url),
//     )
//
// ## Protocol Flow
//
// ### Obtain SFU Information
//
// Before a call can be joined or created, SFU information and an authentication
// token need to be obtained via the Directory Server API. The obtained
// information includes the following items referenced in subsequent sections:
//
// - _SFU Base URL_: Base URL used to create and distribute new calls.
// - _Allowed SFU Hostname Suffixes_: A set of allowed hostname suffixes to be
//   applied against the _SFU Base URL_ when joining calls.
// - _SFU Token_: An opaque token used to authenticate against the SFU.
//
// When receiving the SFU information, ensure the _SFU Base URL_ uses the scheme
// `https` and the included hostname ends with one of the _Allowed SFU Hostname
// Suffixes_.
//
// ### Scoped to Group
//
// #### Periodic Refresh
//
// The following steps are defined as the _Group Call Refresh Steps_ and will be
// applied to update the group calls that are currently considered running
// within a group, determining which one of them is the chosen call and
// potentially join the chosen call:
//
// 1. Let `running` be the list of group calls that are currently considered
//    running within the group.
// 2. Let `calls` be a copy of `running`. Reset the _token-refreshed_ mark of
//    each `call` of `calls` (or simply scope it to the execution of these
//    steps).
// 3. For each `call` of `calls`, run the following steps (labelled _peek-call_)
//    concurrently and wait for them to return:
//    1. If the user is currently participating in `call`, abort the _peek-call_
//       sub-steps.
//    2. _Peek_ the `call` via a `SfuHttpRequest.Peek` request. If this does not
//       result in a response within 5s, remove `call` from `calls` and abort
//       the _peek-call_ sub-steps.
//    3. If the received status code for `call` is `401` and `call` is not
//       marked with _token-refreshed_:
//       1. Refresh the _SFU Token_. If the _SFU Token_ refresh fails or does
//          not yield an _SFU Token_ within 10s, remove `call` from `calls` and
//          abort the _peek-call_ sub-steps.
//       2. Mark the `call` as _token-refreshed_.
//       3. Restart the _peek-call_ sub-steps for this `call`.
//    4. If the server could not be reached or the received status code is not
//       `200` or if the _Peek_ response could not be decoded:
//       1. Remove `call` from `calls`.
//       2. If the received status code is `404`, remove `call` from `running`
//          and abort the _peek-call_ sub-steps.
//       3. If the `call`'s _failed_ counter is `>= 3` and the `call` was
//          received more than 10h ago, remove `call` from `running` and abort
//          the _peek-call_ sub-steps.
//       4. Increase the _failed_ counter for `call` by `1` and abort the
//          _peek-call_ sub-steps.
//    5. Reset the `call`'s _failed_ counter to `0`.
//    6. If the protocol version of the `call` is not supported, remove `call`
//       from `calls`, log a warning that a group call with an unsupported
//       version is currently running and abort the _peek-call_ sub-steps.
//    7. (`call` is kept in `calls` and in `running`.)
// 4. If `running` is empty, cancel the timer to periodically re-run the _Group
//    Call Refresh Steps_ of this group. Otherwise, restart or schedule the
//    timer to re-run the _Group Call Refresh Steps_ of this group in 10s.
// 5. Let `chosen-call` be any call of `calls` with the highest `started_at`
//    value (i.e. the most recently created call) as provided by the _peek_
//    result.
// 6. If `chosen-call` is not defined, signal that no group call is currently
//    running within the group, abort these steps and return `chosen-call`.
// 7. Signal `chosen-call` as the currently running group call within the group.
// 8. If the _Group Call Join Steps_ are currently running with a different (or
//    new) group call than `chosen-call`, cancel and restart the _Group Call
//    Join Steps_ asynchronously with the same `intent` but with the
//    `chosen-call`.
// 9. If the user is currently participating in a group call of this group that
//    is different to `chosen-call`, exit the running group call and run the
//    _Group Call Join Steps_ asynchronously with the `intent` to _only join_
//    `chosen-call`.
// 10. Return `chosen-call`.
//
// Note: The above steps have been carefully crafted to gracefully handle cases
// where the SFU of one call cannot be reached for a short period of time.
//
// When the Threema app is active, run the _Group Call Refresh Steps_ for each
// group. This will start a timer to refresh any group call status.
//
// When the user leaves a group call, run the _Group Call Refresh Steps_ for the
// respective group.
//
// The above described timer may be cancelled when the Threema app is inactive.
// The timer interval may be increased to 30s in case the group conversation is
// currently not visible to the user.
//
// #### Create or Join
//
// The following steps are to be run when a user wants to join a group call of a
// group where a group call is currently considered running (e.g. the user hits
// _join_ in the UI) or when the user intents to create a group call for a group
// where no group call is currently considered running (e.g. the user hits the
// _call_ button in the UI):
//
// 1. Let `intent` be the user's intent, i.e. to either _only join_ or _create
//    or join_ a group call.
// 2. Refresh the _SFU Token_ if necessary. If the _SFU Token_ refresh fails
//    within 10s, abort these steps and notify the user.
// 3. Run the _Group Call Refresh Steps_ for the respective group and let `call`
//    be the result.
// 4. If `call` is undefined and `intent` is to _only join_, abort these steps
//    and notify the user that no group call is running / the group call is no
//    longer running.
// 5. If `call` is undefined, create (but don't send) a `GroupCallStart`
//    message, apply it to `call` and mark `call` as _new_.
// 6. Run the _Group Call Join Steps_ with the `intent` and `call`.
//
// The following steps are defined as the _Group Call Join Steps_ (also applied
// for creating a group call).:
//
// 1. Let `intent` be either _only join_ or _create or join_. Let `call` be the
//    given group call to be joined (or created).
// 2.  _Join_ (or implicitly create) the group call via a `SfuHttpRequest.Join`
//    request. If this does not result in a response within 10s, abort these
//    steps and notify the user.
// 3. If the received status code is `503`, notify the user that the group call
//    is full and abort these steps.
// 4. If the server could not be reached or the received status code is not
//    `200` or if the _Join_ response could not be decoded, abort these steps
//    and notify the user.
// 5. Establish a WebRTC connection to the SFU with the information provided in
//    the _Join_ response. Wait until the SFU sent the initial
//    `SfuToParticipant.Hello` message via the associated data channel. Let
//    `hello` be that message.
// 6. If the `hello.participants` contains less than 4 items, set the initial
//    capture state of the microphone to _on_.
// 7. If `call` is marked as _new_:
//    1. Optionally add an artificial wait period of 2s minus the time elapsed
//       since step 1.[^1]
//    2. Let `message-id` be a random message ID.
//    3. Schedule a persistent task to run he _Bundled Messages Send Steps_ with
//       the following properties:
//       - `id` set to `message-id`,
//       - `receivers` set to all group members that have `GROUP_CALL_SUPPORT`,
//       - to construct a `GroupCallStart` message from `call`.
//    4. Add the created `call` to the list of group calls that are currently
//       considered running.
//    5. Asynchronously run the _Group Call Refresh Steps_.[^2]
// 8. The group call is now considered established and should asynchronously
//    invoke the SFU to Participant and Participant to Participant flows.
//
// [^1]: This prevents butter-fingered user from accidentally starting a group
// call.
//
// [^2]: This will initiate the refresh timer for a newly created call and
// signal it to the UI.
//
// Note: Implementations need to ensure that only one group call can be active
// at the same time in the application. This means that only one invocation of
// the _Create or Join_ flow and only one invocation of the _Group Call Join
// Steps_ can be active. Be aware that these steps can be cancelled by the user
// and by the _Group Call Refresh Steps_.
//
// ### SFU to Participant Flow
//
// Upon successful joining via `SfuHttpRequest.Join`, the SFU waits for the
// client to establish a WebRTC connection and then announces all participants
// to the newly joined participant in its `SfuToParticipant.Hello` message.
//
// When another participant joins or leaves, a `ParticipantJoined` or
// `ParticipantLeft` message will be sent.
//
// At any time, participants may subscribe and unsubscribe receiving microphone,
// camera and screen data from other participants.
//
// If the user is alone in a call for more than 3 minute, the call should be
// left to save resources. The SFU will automatically drop such calls after 5
// minutes but this results in non-ideal UX.
//
// ### Participant to Participant Flow
//
// Unlike the other flows, this one is more complicated and needs to be done
// separately for each other participant. During the handshake, ephemeral
// encryption keys will be established.
//
// Note that multiple participants with the same Threema ID in the same call are
// **explicitly allowed**. Not only can this happen in case the connection has
// been lost (e.g. the client already reconnected but the SFU has not detected
// connection loss yet), but it is also a feature for multi-device capable
// clients.
//
// #### Handshake
//
// When a new participant (NP) joins, it must authenticate each other existing
// participant (EP) and establish an ephemeral shared secret (`PCK`). The flow
// depends on whether NP and EP are normal or guest participants:
//
// If both are normal participants:
//
//     NP ----- Hello ---> EP
//     NP <---- Hello ---- EP
//     NP <---- Auth ----- EP
//     NP ----- Auth ----> EP
//
// If both are guest participants:
//
//     NP -- GuestHello -> EP
//     NP <- GuestHello -- EP
//     NP <- GuestAuth --- EP
//     NP -- GuestAuth --> EP
//
// If NP is a normal participant and EP is a guest participant:
//
//     NP ----- Hello ---> EP
//     NP <- GuestHello -- EP
//     NP <- GuestAuth --- EP
//     NP -- GuestAuth --> EP
//
// If NP is a guest participant and EP is a normal participant:
//
//     NP -- GuestHello -> EP
//     NP <---- Hello ---- EP
//     NP <- GuestAuth --- EP
//     NP -- GuestAuth --> EP
//
// Note: This looks more intimidating than it really is. Basically, if either is
// a guest, we fulfill the guest handshake but both always start with sending
// their respective role's _hello_ variant.
//
// For group calls scoped to groups:
//
// - Only handshake messages from Threema IDs that are part of the group are
//   allowed.
// - External guests are not allowed and therefore the guest handshake is not
//   allowed.
//
// #### Post-Handshake
//
// After the handshake, **both** sides run the following steps:
//
// 1. Subscribe to the other participant's microphone feed (i.e. send a
//    `ParticipantMicrophone` message to the SFU).
// 2. If the user is an administrator, send an `Admin.ReportAsAdmin` message to
//    the other participant.
// 3. If _hold_ is currently active, send a `Hold` message to the other
//    participant.
// 4. If _hold_ is not currently active, send a `CaptureState` message to the
//    other participant for each device (camera, microphone, ...) that is
//    currently activated (`Mode` is `ON`).
//
// #### Join/Leave of Other Participants
//
// When a new participant joins, all other participants run the following steps:
//
// 1. Let `pcmk` be the currently _applied_ PCMK with the associated context.
// 2. If the amount of ratchet rounds for `pcmk` is `255`, abort the call with
//    an error condition and abort these steps.
// 3. Advance the ratchet of `pcmk` once (i.e. replace the key by deriving
//    PCMK') and apply for media encryption immediately. Note: Do **not** reset
//    the MFSN!
// 4. Set the _handshake state_ of this participant to `await-np-hello`.
//
// Note: The announcement of the new participant is guaranteed to be sent prior
// to any handshake messages of the new participant.
//
// When a participant leaves, all other participants run the following steps:
//
// 1. Let `pending-pcmk` be the currently _pending_ PCMK the associated context.
// 2. If `pending-pcmk` exists, additionally mark `pending-pcmk` as _stale_ and
//    abort these steps.
// 3. Let `current-pcmk` be the currently _applied_ PCMK with the associated
//    context.
// 4. Set `pending-pcmk` in the following way:
//    1. Generate a new cryptographically secure random PCMK and assign it to
//       `pending-pcmk`.
//    2. Set `pending-pcmk.epoch` to `current-pcmk.epoch + 1`, wrap back to `0`
//       if it would be `256`.
//    3. Set `pending-pcmk.ratchet_counter` to `0`.
//    4. Do **not** reset the MFSN! Continue the existing MFSN counter of the
//       previous PCMK.
// 5. Send `pending-pcmk` to all authenticated participants via a _rekey_
//    message.
// 6. Schedule a volatile task bound to the call to run the following steps
//    after 2s:
//    1. Apply `pending-pcmk` for media encryption. This means that
//       `pending-pcmk` now replaces the _applied_ PCMK and is no longer
//       _pending_.
//    2. If `pending-pcmk` is marked as _stale_, run the parent steps from the
//       beginning.
//
// When a participant receives a _rekey_ message from another participant.
//
// 1. Let `current-pcmk` be the PCMK and its associated context used for the
//    participant.
// 2. Let `new-pcmk` be the media keys (PCMK) of the received message.
// 3. Store `new-pcmk` as a successor to `current-pcmk` (and any other successor
//    already stored on `current-pcmk`) and follow the description of the media
//    frame on when to apply it.
//
// Note: The result of the above steps is that re-keying is throttled but always
// catches up to the current participant state with a maximum delay of 4s.
//
// #### State Update
//
// One of the participants is deterministically designated to update the
// peekable call state every 10s and additionally every time a participant joins
// or leaves. If the call state has not been updated/refreshed for 30s, the SFU
// will delete it.
//
// After each change to the list of participants, run the following steps to
// determine whether the user is designated:
//
// 1. Cancel any running timer to update the call state.
// 2. Let `candidates` be a list of all currently authenticated non-guest
//    participants.
// 3. If `candidates` is empty, add all currently authenticated guest
//    participants to the list.
// 4. If the user is not in `candidates`, abort these steps.
// 5. If the user does not have the lowest participant ID in `candidates`, abort
//    these steps.
// 6. Send a `ParticipantToSfu.UpdateCallState` message to the SFU and schedule
//    a repetitive timer to repeat this step every 10s.
//
// Note: The above algorithm is prone to races since the authentication process
// is asynchronous for each participant pair. However, this should not be an
// issue as they'd essentially post the same status (eventually).

syntax = "proto3";

package group_call;

option java_package = "ch.threema.protobuf.group_call";
option java_multiple_files = true;
option php_namespace = "Threema\\Protocols\\GroupCall";

import "common.proto";

// Current call state as announced by the designated client.
//
// Note: The `CallState` accurateness must not be relied upon as it can be out
//       of date and can be replayed by the SFU.
message CallState {
  // Random amount of padding, ignored by the receiver.
  bytes padding = 1;

  // Participant ID of the designated client that created this message.
  uint32 state_created_by = 2;

  // UNIX-ish timestamp in milliseconds the designated client created this
  // message.
  uint64 state_created_at = 3;

  // Information for a single participant.
  message Participant {
    reserved 1; // Redundant participant ID

    // A _normal_ participant, i.e. a Threema client.
    message Normal {
      // Threema ID of the sender.
      string identity = 1;

      // Nickname associated to the Threema ID (without `~` prefix).
      string nickname = 2;
    }

    // A _guest_ participant.
    message Guest {
      // The guest's self-assigned name.
      string name = 1;
    }

    // Type-specific information.
    oneof participant {
      Normal threema = 2;
      Guest guest = 3;
    }
  }

  // Information for each participant of the group call.
  map<uint32, Participant> participants = 4;
}

// Supported feature (to be used as a bitmask).
enum SupportedFeature {
  // Base feature support (always present).
  BASE = 0x0000;
  // Support for screen sharing.
  SCREEN_SHARE = 0x0001;
}

// Request payloads sent to the SFU as part of an HTTP request.
message SfuHttpRequest {
  // Peeks for the current state of the group call for the given Group Call ID.
  //
  // IMPORTANT: The _peek_ process is considered stable across different
  // protocol versions. Therefore, the message **should** maintain backwards
  // compatibility!
  //
  // The URL is formed in the following way:
  //
  //     <sfu_base_url>/v1/peek/<call_id-as-hex>
  //
  // When sending this request:
  //
  // 1. Use `POST` as method.
  // 2. Set the `Authorization` header to `ThreemaSfuToken <sfu-token>`.
  // 3. Set the encoded `SfuHttpRequest.Peek` message as body.
  //
  // When receiving this request:
  //
  // 1. If the `Authorization` header is missing, the provided `sfu-token` in
  //    the `Authorization` header is invalid or expired, respond with status
  //    code `401` and abort these steps.
  // 2. If the provided data is invalid, respond with status code `400` and
  //    abort these steps.
  // 3. If `call_id` does not equal the Call ID from the URL (decoded
  //    `call_id-as-hex`), respond with status code `400` and abort these steps.
  // 4. If no group call for the given `call_id` is currently running, respond
  //    with status code `404` and abort these steps.
  // 5. Respond with status code `200` and an encoded `SfuHttpResponse.Peek`
  //    message as body.
  message Peek {
    // Group Call ID associated to the group call.
    bytes call_id = 1;
  }

  // Requests to join the group call with the given Group Call ID.
  //
  // The URL is formed in the following way:
  //
  //     <sfu_base_url>/v1/join/<call_id-as-hex>
  //
  // When sending this request:
  //
  // 1. Use `POST` as method.
  // 2. Set the `Authorization` header to `ThreemaSfuToken <sfu-token>`.
  // 3. Set the encoded `SfuHttpRequest.Join` message as body.
  //
  // When receiving this request:
  //
  // 1. If the `Authorization` header is missing, the provided `sfu-token` in
  //    the `Authorization` header is invalid or expired, respond with status
  //    code `401` and abort these steps.
  // 2. If the provided data is invalid, respond with status code `400` and
  //    abort these steps.
  // 3. If `call_id` does not equal the Call ID from the URL (decoded
  //    `call_id-as-hex`), respond with status code `400` and abort these steps.
  // 4. If the `protocol_version` is unsupported by the SFU, respond with status
  //    code `419` and abort these steps.
  // 5. If no more participants can join the group call for the given `call_id`,
  //    respond with status code `503` and abort these steps.
  // 6. Respond with status code `200` and an encoded `SfuHttpResponse.Join`
  //    message as body.
  // 7. Once the WebRTC connection has been established, announce the newly
  //    joined participant to all other participants via the corresponding data
  //    channel. If no WebRTC connection is being established within 30s, the
  //    participant ID is no longer reserved for the client and the group call
  //    must be teared down if no other participant started joining this group
  //    call.
  message Join {
    // Group Call ID associated to the group call.
    bytes call_id = 1;

    // Protocol version the call was announced with.
    uint32 protocol_version = 2;

    // DTLS fingerprint of the x509 certificate that will be used by the client.
    //
    // Note: This is the authentication anchor for the WebRTC connection towards
    //       the SFU.
    bytes dtls_fingerprint = 3;
  }
}

// Response payloads sent back from the SFU as part of an HTTP request.
message SfuHttpResponse {
  // Information returned for a running group call.
  //
  // IMPORTANT: The _peek_ process is considered stable across different
  // protocol versions. Therefore, the message **should** maintain backwards
  // compatibility!
  //
  // Note: The included `CallState` information may not be accurate and should
  // not be relied upon.
  message Peek {
    // Unix-ish timestamp in milliseconds for when the first participant joined
    // the Group Call ID and therefore started the group call.
    uint64 started_at = 1;

    // Maximum amount of participants allowed in the group call.
    uint32 max_participants = 2;

    // Call state (`CallState`), encrypted by `GCSK.secret` and prefixed with a
    // random nonce.
    //
    // Not provided in case the call is currently running but no participant has
    // sent a call state to the SFU, or if the call state expired.
    //
    // The content of the call state is protocol version dependent and should
    // therefore be ignored if a client does not support the particular protocol
    // version the group call is associated with.
    optional bytes encrypted_call_state = 3;
  }

  // Information returned when joining a group call.
  //
  // When receiving this response, initiate the WebRTC connection to the SFU and
  // consider the connection established when the `SfuToParticipant.Hello`
  // message has been received on the associated data channel.
  message Join {
    // Unix-ish timestamp in milliseconds for when the first participant joined
    // the Group Call ID and therefore started the group call.
    uint64 started_at = 1;

    // Maximum amount of participants allowed in the group call.
    uint32 max_participants = 2;

    // Participant ID assigned to the client.
    //
    // Note: The client needs to know the participant ID early to derive MIDs
    //       required to be present in the O/A SDP.
    uint32 participant_id = 3;

    // Address the SFU is listening for a WebRTC connection.
    message Address {
      // Protocol.
      enum Protocol { UDP = 0; }
      Protocol protocol = 1;

      // Port.
      uint32 port = 2;

      // IPv4 or IPv6 address.
      string ip = 3;
    }

    // List of addresses the SFU listens for a WebRTC connection.
    //
    // Note: One UDP IPv4 address is mandatory! One IPv6 address is recommended.
    repeated Address addresses = 4;

    // ICE username fragment for the WebRTC connection.
    string ice_username_fragment = 5;

    // ICE password for the WebRTC connection.
    string ice_password = 6;

    // DTLS fingerprint of the x509 certificate that will be used by the SFU.
    //
    // Note: This is the authentication anchor for the WebRTC connection towards
    //       the SFU.
    bytes dtls_fingerprint = 7;

    // RTP header extension ID mapping.
    //
    // Note: The same ID is applied for header extensions used for both microphone and
    // camera/screen.
    message RtpHeaderExtensionIds {
      // urn:ietf:params:rtp-hdrext:sdes:mid
      uint32 mid = 1;

      // urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
      uint32 rtp_stream_id = 2;

      // urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
      uint32 repaired_rtp_stream_id = 3;

      // http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
      uint32 absolute_send_time = 4;

      // http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
      uint32 transport_wide_congestion_control_01 = 5;

      // urn:3gpp:video-orientation
      uint32 video_orientation = 6;

      // urn:ietf:params:rtp-hdrext:toffset
      uint32 time_offset = 7;
    }
    RtpHeaderExtensionIds rtp_header_extension_ids = 9;

    // Supported features bitmask (see `SupportedFeature`).
    uint64 supported_features = 8;
  }
}

// Messages sent from the SFU to a participant via a data channel.
//
// Data Channel Parameters:
//
// - `ordered`: `true`
// - `negotiated`: `true`
// - `id`: `0`
message SfuToParticipant {
  // The enveloped message from the SFU.
  //
  // When relaying a message from one participant to another, omit any
  // additional padding.
  //
  // IMPORTANT: The format of the `SfuToParticipant.Envelope` and
  // `ParticipantToSfu.Envelope` must be compatible for the relay case, so the
  // SFU can forward the data without having to re-encode.
  message Envelope {
    // Random amount of padding, ignored by the receiver.
    bytes padding = 1;

    oneof content {
      ParticipantToParticipant.OuterEnvelope relay = 2;
      Hello hello = 3;
      Timestamp timestamp_response = 6;
      ParticipantJoined participant_joined = 4;
      ParticipantLeft participant_left = 5;
    }
  }

  // Announces all other participants to a newly joined participant.
  //
  // When receiving this message:
  //
  // 1. If a `Hello` was received before (i.e. if the receiver is not a newly
  //    joined participant), log a warning and abort these steps.
  // 2. Initiate the participant to participate handshake for each participant
  //    listed in this message.
  message Hello {
    // All participants in the group call. This **excludes** the client's
    // participant ID.
    repeated uint32 participant_ids = 1;
  }

  // Announces that a new participant joined to existing participants.
  //
  // When receiving this message:
  //
  // 1. Look up the participant. If it already exists (i.e. never _left_), log a
  //    warning and abort these steps.
  // 2. Run the corresponding steps described by the _Join/Leave_ section.
  message ParticipantJoined { uint32 participant_id = 1; }

  // Announces that a participant left to existing participants.
  //
  // When receiving this message:
  //
  // 1. Look up the participant. If it was never announced to have _joined_ by
  //    an associated `ParticipantJoined` message, log a warning and abort these
  //    steps.
  // 2. Run the corresponding steps described by the _Join/Leave_ section.
  message ParticipantLeft { uint32 participant_id = 1; }

  // Current UNIX-ish timestamp in milliseconds of the SFU.
  //
  // When receiving this message:
  //
  // 1. Resolve the first pending timestamp request with the response.
  message Timestamp { uint64 ms = 1; }
}

// Messages sent from a participant to the SFU via a data channel.
//
// Data Channel Parameters:
//
// - `ordered`: `true`
// - `negotiated`: `true`
// - `id`: `0`
message ParticipantToSfu {
  // The enveloped message towards the SFU.
  //
  // When relaying a message from one participant to another, omit any
  // additional padding.
  //
  // IMPORTANT: The format of the `SfuToParticipant.Envelope` and
  // `ParticipantToSfu.Envelope` must be compatible for the relay case, so the
  // SFU can forward the data without having to re-encode.
  message Envelope {
    // Random amount of padding, ignored by the receiver.
    bytes padding = 1;

    oneof content {
      ParticipantToParticipant.OuterEnvelope relay = 2;
      UpdateCallState update_call_state = 3;
      RequestTimestamp request_timestamp = 7;
      ParticipantMicrophone request_participant_microphone = 6;
      ParticipantCamera request_participant_camera = 4;
      ParticipantScreen request_participant_screen = 5;
    }
  }

  // Request the current timestamp of the SFU.
  //
  // When receiving this message:
  //
  // 1. Respond with a `TimestampResponse` message.
  message RequestTimestamp {}

  // Update the call state that can be retrieved via a _peek_.
  //
  // Note: Only the currently designated client should send this to the SFU.
  //
  // When receiving this message:
  //
  // 1. Store the encrypted call state and make it accessible via _peek_ HTTP
  //    requests.
  // 2. Start a timer to purge the call state after 30s. Subsequent
  //    `UpdateCallState` messages will update the call state and reset the
  //    timer.
  message UpdateCallState {
    // Call state (`CallState`), encrypted by `GCSK` and prefixed with
    // a random nonce.
    bytes encrypted_call_state = 1;
  }

  // Subscribe or unsubscribe to a participant's microphone feed.
  //
  // When receiving this message:
  //
  // 1. If the `participant_id` refers to the sender's participant ID or an
  //    unknown participant ID, discard the message and abort these steps.
  // 2. If `subscribe` is set, forward the microphone feed to the client that
  //    fits best to the provided parameters.
  // 3. If `unsubscribe` is set, stop forwarding microphone feed of this
  //    participant to the client.
  message ParticipantMicrophone {
    // Participant ID whose microphone feed should be subscribed or unsubscribed
    // from.
    uint32 participant_id = 1;

    // Subscribe to a participant's microphone feed.
    message Subscribe {}

    // Unsubscribe a participant's microphone feed.
    message Unsubscribe {}

    oneof action {
      Subscribe subscribe = 2;
      Unsubscribe unsubscribe = 3;
    }
  }

  // Subscribe or unsubscribe to a participant's camera feed.
  //
  // When receiving this message:
  //
  // 1. If the `participant_id` refers to the sender's participant ID or an
  //    unknown participant ID, discard the message and abort these steps.
  // 2. If `subscribe` is set, forward the camera feed to the client that fits
  //    best to the provided parameters.
  // 3. If `unsubscribe` is set, stop forwarding camera feed of this participant
  //    to the client.
  message ParticipantCamera {
    // Participant ID whose camera feed should be subscribed or unsubscribed
    // from.
    uint32 participant_id = 1;

    // Subscribe to a participant's camera feed.
    message Subscribe {
      // Desired resolution. The client should use the canvas' resolution the
      // camera feed be displayed in. The SFU will select the spatial layer that
      // fits best.
      common.Resolution desired_resolution = 1;

      // Desired frame rate. The SFU will select the temporal layer that fits
      // best.
      uint32 desired_fps = 2;
    }

    // Unsubscribe a participant's camera feed.
    message Unsubscribe {}

    oneof action {
      Subscribe subscribe = 2;
      Unsubscribe unsubscribe = 3;
    }
  }

  // Subscribe or unsubscribe to a participant's screen feed.
  //
  // Availability: If the SFU announced support for
  // `SupportedFeature.SCREEN_SHARE`.
  //
  // When receiving this message:
  //
  // 1. If the `participant_id` refers to the sender's participant ID or an
  //    unknown participant ID, discard the message and abort these steps.
  // 2. If `subscribe` is set, forward the screen feed to the client that fits
  //    best to the provided parameters.
  // 3. If `unsubscribe` is set, stop forwarding screen feed of this participant
  //    to the client.
  message ParticipantScreen {
    // Participant ID whose screen feed should be subscribed or unsubscribed
    // from.
    uint32 participant_id = 1;

    // Subscribe to a participant's screen feed.
    message Subscribe {
      // Desired resolution. The client should use the canvas' resolution the
      // screen feed be displayed in. The SFU will select the spatial layer that
      // fits best.
      common.Resolution desired_resolution = 1;

      // Desired frame rate. The SFU will select the temporal layer that fits
      // best.
      uint32 desired_fps = 2;
    }

    // Unsubscribe a participant's screen feed.
    message Unsubscribe {}

    oneof action {
      Subscribe subscribe = 2;
      Unsubscribe unsubscribe = 3;
    }
  }
}

// Messages sent from one participant to another.
//
// Note that these are relayed via `SfuToParticipant.Envelope` and
// `ParticipantToSfu.Envelope` in order to prevent races with
// `ParticipantJoined`/`ParticipantLeft`.
message ParticipantToParticipant {
  // Used for all messages that are relayed from one participant to another via
  // the SFU.
  //
  // When receiving a relayed message:
  //
  // 1. If the `receiver` is not the user's assigned participant id, discard the
  //    message and abort these steps.
  // 2. If the `sender` is unknown, discard the message and abort these steps.
  // 3. Decrypt `encrypted_data` according to the current _handshake state_ and
  //    handle the inner envelope:
  //    - `await-ep-hello` or `await-np-hello`: Expect a
  //      `Handshake.HelloEnvelope`.
  //    - `await-auth`: Expect a `Handshake.AuthEnvelope`.
  //    - `done`: Expect a post-auth `Envelope`.
  message OuterEnvelope {
    // Participant ID of the sender. Checked by the SFU to be correct, dropped
    // if not.
    uint32 sender = 1;

    // Participant ID of the receiver. Checked by the SFU to exist, dropped if
    // not.
    uint32 receiver = 2;

    // The inner envelope. Always encrypted. Key and nonce are to be inferred
    // from the current _handshake state_ towards the sending participant.
    bytes encrypted_data = 4;
  }

  // Messages required for the initial lock-step handshake between participants.
  message Handshake {
    // The first message (`HelloEnvelope(Hello)` or `HelloEnvelope(GuestHello)`)
    // of both sides is always encrypted by `GCHK`, prefixed with a
    // random nonce.
    message HelloEnvelope {
      // Random amount of padding, ignored by the receiver
      bytes padding = 1;

      oneof content {
        Hello hello = 2;
        GuestHello guest_hello = 3;
      }
    }

    // If both sides started the normal handshake, the second message is
    // encrypted in the following way:
    //
    // 1. Let `inner-nonce` be a random nonce.
    // 2. Let `inner-data` be encrypted by:
    //
    // ```text
    // S = X25519HSalsa20(<sender.CK>.secret, <receiver.CK>.public)
    // GCNHAK = Blake2b(
    //   key=S, salt='nha', personal='3ma-call', input=GCKH)
    // XSalsa20-Poly1305(
    //   key=GCNHAK,
    //   nonce=<inner-nonce>,
    //   data=<AuthEnvelope(Auth)>,
    // )
    // ```
    // 3. Let `outer-data` be encrypted by:
    //
    // ```text
    // XSalsa20-Poly1305(
    //   key=X25519HSalsa20(<sender.PCK>.secret, <receiver.PCK>.public),
    //   nonce=<sender.PCCK> || <sender.PCSN+>,
    //   data=<inner-nonce> || <inner-data>,
    // )
    // ```
    // 4. Return `outer-data`.
    //
    // If either side started the guest handshake, the second message is
    // encrypted by:
    //
    // ```text
    // XSalsa20-Poly1305(
    //   key=X25519HSalsa20(<sender.PCK>.secret, <receiver.PCK>.public),
    //   nonce=<sender.PCCK> || <sender.PCSN+>,
    //   data=<AuthEnvelope(GuestAuth)>,
    // )
    // ```
    //
    // When receiving this message:
    //
    // 1. If either side initiated a guest handshake via a `GuestHello`, expect
    //    `guest_auth` to be set. If `guest_auth` is not set, log a warning and
    //    abort these steps.
    // 2. If both sides initiated the (normal) handshake, expect `auth` to be
    //    set. If `auth` is not set, log a warning and abort these steps.
    message AuthEnvelope {
      // Random amount of padding, ignored by the receiver
      bytes padding = 1;

      oneof content {
        Auth auth = 2;
        GuestAuth guest_auth = 3;
      }
    }

    // Initial handshake message.
    //
    // When creating this message as a newly joined participant towards another
    // participant:
    //
    // 1. Set the participant's _handshake state_ to `await-ep-hello`.
    // 2. Send this message.
    //
    // When receiving this message as a guest participant:
    //
    // 1. Map it to a `GuestHello` in the following way:
    //    - `name`: `Hello.nickname`
    //    - `pck`: `Hello.pck`
    //    - `pcck`: `Hello.pcck`
    // 2. Handle the mapped `GuestHello` as if it had been received directly.
    //
    // When receiving this message as a regular participant:
    //
    // 1. (Placeholder for conference call PCK != GCAMK step.)
    // 2. If the group call is scoped to a (Threema) group and `identity` is not
    //    part of the associated group (including the user itself), log a
    //    warning and abort these steps.
    // 3. If the sender is a newly joined participant and therefore the
    //    _handshake state_ was set to `await-np-hello` (as described by the
    //    _Join/Leave_ section):
    //    1. Respond by sending a `Hello` message, immediately followed by an
    //       `Auth` message.
    //    2. Set the participant's _handshake state_ to `await-auth` and abort
    //       these steps.
    // 4. If the participant's _handshake state_ is `await-ep-hello`:
    //    1. If the `pck` reflects the local PCK.public or the `pcck` reflects
    //       the local PCCK, log a warning and abort these steps.
    //    2. Respond by sending an `Auth` message.
    //    3. Set the participant's _handshake state_ to `await-auth` and abort
    //       these steps.
    // 5. Log a warning and abort these steps.
    message Hello {
      // Threema ID of the sender.
      string identity = 1;

      // Nickname associated to the Threema ID (without `~` prefix).
      string nickname = 2;

      // 32 byte ephemeral public key (`PCK.public`) towards the remote
      // participant.
      //
      // Note: It is allowed to use the same `PCK` for multiple participants.
      bytes pck = 3;

      // 16 byte random cookie used for nonces by the sender in subsequent
      // messages.
      bytes pcck = 4;
    }

    // Second and final handshake message.
    //
    // When receiving this message:
    //
    // 1. If the participant's _handshake state_ is not `await-auth`, log a
    //    warning and abort these steps.
    // 2. If the repeated `pck` does not equal the local `PCK.public` used
    //    towards this participant, log a warning and abort these steps.
    // 3. If the repeated `pcck` does not equal the local `PCCK` used towards
    //    this participant, log a warning and abort these steps.
    // 4. Set the participant's _handshake state_ to `done`.
    message Auth {
      // 32 byte repeated ephemeral public key from the `Hello` message.
      //
      // Note: Repeating the sender's `PCK.public` prevents replay attacks.
      bytes pck = 1;

      // 32 byte repeated random cookie from the `Hello` message.
      //
      // Note: Repeating the sender's `PCCK` prevents replay attacks while
      //       allowing the sender to use the same `PCK` for multiple
      //       participants.
      bytes pcck = 2;

      // The currently applied PCMK and any _pending_ PCMK used for media
      // encryption, specifically in that order.
      //
      // Note: An implementation can expect at least one media key to be
      // present.
      repeated MediaKey media_keys = 3;
    }

    // Initial guest handshake message.
    //
    // When creating this message as a newly joined guest participant towards
    // another participant:
    //
    // 1. Set the participant's _handshake state_ to `await-ep-hello`.
    // 2. Send this message.
    //
    // When receiving this message:
    //
    // 1. If guest participants are not allowed for this call, log a warning
    //    and abort these steps.
    // 2. (Placeholder for conference call PCK != GCAMK step.)
    // 3. If the sender is a newly joined participant and therefore the
    //    _handshake state_ was set to `await-np-hello` (as described by the
    //    _Join/Leave_ section):
    //    1. Respond by sending a `GuestHello` message, immediately followed by
    //       a `GuestAuth` message.
    //    2. Set the participant's _handshake state_ to `await-guest-auth` and
    //       abort these steps.
    // 4. If the participant's _handshake state_ is `await-ep-hello`:
    //    1. If the `pck` reflects the local PCK.public or the `pcck` reflects
    //       the local PCCK, log a warning and abort these steps.
    //    2. Respond by sending a `GuestAuth` message.
    //    3. Set the participant's _handshake state_ to `await-guest-auth` and
    //       abort these steps.
    // 5. Log a warning and abort these steps.
    message GuestHello {
      // The guest's self-assigned name.
      string name = 1;

      // 32 byte ephemeral public key (`PCK.public`) towards the remote
      // participant.
      //
      // Note: It is allowed to use the same `PCK` for multiple participants.
      bytes pck = 2;

      // 16 byte random cookie used for nonces by the sender in subsequent
      // messages.
      bytes pcck = 3;
    }

    // Second and final handshake message triggered if either side initiated the
    // guest handshake.
    //
    // When receiving this message:
    //
    // 1. If the participant's _handshake state_ is not `await-guest-auth`, log
    //    a warning and abort these steps.
    // 2. If the repeated `pck` does not equal the local `PCK.public` used
    //    towards this participant, log a warning and abort these steps.
    // 3. If the repeated `pcck` does not equal the local `PCCK` used towards
    //    this participant, log a warning and abort these steps.
    // 4. Set the participant's _handshake state_ to `done`.
    message GuestAuth {
      // 32 byte repeated ephemeral public key from the `GuestHello` message.
      //
      // Note: Repeating the sender's `PCK.public` prevents replay attacks.
      bytes pck = 1;

      // 32 byte repeated random cookie from the `GuestHello` message.
      //
      // Note: Repeating the sender's `PCCK` prevents replay attacks while
      //       allowing the sender to use the same `PCK` for multiple
      //       participants.
      bytes pcck = 2;

      // The currently applied PCMK and any _pending_ PCMK used for media
      // encryption, specifically in that order.
      //
      // Note: An implementation can expect at least one media key to be
      // present.
      repeated MediaKey media_keys = 3;
    }
  }

  // After fulfilling either the (normal) handshake or the guest handshake, all
  // following messages are encoded in `Envelope` and encrypted by:
  //
  // ```text
  // XSalsa20-Poly1305(
  //   key=X25519HSalsa20(<sender.PCK>.secret, <receiver.PCK>.public),
  //   nonce=<sender.PCCK> || <sender.PCSN+>,
  // )
  // ```
  //
  // Note: Since the guest handshake is TOFU, an attacker knowing `GCK` having
  // control over the SFU may apply a MITM attack between a guest participant
  // and another participant. The attacker would be able to silently eavesdrop
  // all media traffic between the two participants. This is repeatable for all
  // other participants and means the attacker is able to silently eavesdrop the
  // whole call. Therefore, if a call is not open for guests, `GuestHello` (and
  // `GuestAuth`) **must not** be accepted.
  //
  // When receiving this message:
  //
  // 1. If the participant's _handshake state_ is not `done`, log a warning and
  //    abort these steps.
  // 2. Handle the message according to the content.
  message Envelope {
    // Random amount of padding, ignored by the receiver
    bytes padding = 1;

    oneof content {
      // An `Admin.Envelope`, encrypted as described by that message.
      bytes encrypted_admin_envelope = 2;

      // Announces new media keys a participant will apply soon.
      MediaKey rekey = 3;

      // Announces capture state changes of a participant.
      CaptureState capture_state = 4;

      // Announces that the participant entered the _hold_ state.
      HoldState hold_state = 5;
    }
  }

  // Messages from admins towards participants (including admins).
  message Admin {
    // Message from an administrator, encrypted by:
    //
    // ```text
    // XSalsa20-Poly1305(
    //   key=X25519HSalsa20(GCAMK.secret, <receiver.PCK>.public),
    //   nonce=<sender.PCCK> || <sender.PCSN+>,
    // )
    // ```
    //
    // IMPORTANT: The `ParticipantToParticipant.Envelope` that encapsulates this
    // message shall be encrypted by the same `PCSN` as used for this
    // `Envelope`. The only difference is that the sender uses `GCAMK` instead
    // of its ephemeral `PCK`.
    message Envelope {
      oneof content {
        ReportAsAdmin report_as_admin = 1;
        PromoteToAdmin promote_to_admin = 2;
        ForceLeave force_leave = 3;
        ForceCaptureStateOff force_capture_state_off = 4;
        ForceFocus force_focus = 5;
      }
    }

    // Report as an administrator.
    //
    // When receiving this message, mark the sender as an administrator in the
    // UI.
    message ReportAsAdmin {}

    // Promote the receiver to an administrator.
    //
    // Note: This is final for the scope of this Group Call. An administrator
    //       cannot be demoted.
    //
    // When receiving this message:
    //
    // 1. If the user already is an administrator, abort these steps.
    // 2. Derive GCAMK and calculate the associated public key from the received
    //    `gcak`. If it does not match the known `GCAMK.public`, log a warning
    //    and abort these steps.
    // 3. Send an `Admin.ReportAsAdmin` message to all other participants
    //    (including the sender who promoted the user to an admin).
    // 4. Notify the user of its admin status and enable administration
    //    functionality in the UI.
    message PromoteToAdmin { bytes gcak = 1; }

    // Force the receiver to leave the call.
    message ForceLeave {}

    // Force the receiver's capture device to be turned off.
    //
    // Note: This is a momentary enforcement. A participant may immediately
    //       restart capturing a device (e.g. unmute itself) and the message is
    //       not repeated towards newly joined participants.
    //
    // When receiving this message:
    //
    // 1. Look up the corresponding device. If none could be found, abort these
    //    steps.
    // 2. If the device's capture state is already _off_, abort these steps.
    // 3. Send a `CaptureState` message for the device and follow the creation
    //    steps of that message (i.e. stop capturing, etc.).
    message ForceCaptureStateOff {
      enum Device {
        // Stop capturing all devices
        ALL = 0;
        // Stop capturing the microphone (i.e. mute)
        MICROPHONE = 1;
        // Stop capturing the camera
        CAMERA = 2;
        // Stop capturing the screen
        SCREEN = 3;
      }
      Device device = 1;
    }

    // Force focus on a specific participant.
    //
    // Note: This is a momentary enforcement. A participant may immediately
    //       remove the focus and the message is not repeated towards newly
    //       joined participants.
    //
    // When receiving this message:
    //
    // 1. Look up the participant to be focused. If none could be found, abort
    //    these steps.
    // 2. Focus the participant in the UI. The camera or screen feed
    //    subscription may need to be created (e.g. participant was not visible
    //    in the viewport before) or updated (e.g. display resolution changes
    //    due to focus) by a corresponding `Subscribe` message sent to the SFU.
    message ForceFocus { uint32 participant_id = 1; }
  }

  // Media keys a participant will use for sending.
  //
  // Will be sent towards new and existing participants as described by the
  // _Join/Leave_ section.
  message MediaKey {
    // The current epoch reflecting the PCMK state.
    //
    // Initially, epoch is `0` and increases each time a participant leaves. The
    // concrete mechanism is explained in the _Join/Leave_ section.
    uint32 epoch = 1;

    // The current ratchet counter reflecting the PCMK state.
    //
    // Initially (or when a participant leaves), the ratchet counter is `0` and
    // increases each time a participant joins. The ratcheting mechanism is
    // explained in the _Join/Leave_ section.
    uint32 ratchet_counter = 2;

    // The current state of the PCMK with the applied ratchet counter.
    //
    // Initially (or when a participant leaves), PCMK is a random 32 byte secret
    // key. The concrete mechanism is explained in the _Join/Leave_ section.
    //
    // This key must be identical **towards** all participants.
    bytes pcmk = 3;
  }

  // Signals a participant's device capturing state.
  //
  // When creating this message:
  //
  // 1. Let `device` be the device whose state is to be updated.
  // 2. If `device` is to be turned _off_:
  //    1. Stop capturing from the device.
  //    2. Pause the corresponding media track.
  // 3. If `device` is to be turned _on_:
  //    1. Start capturing from the device.
  //    2. Resume the corresponding media track.
  // 4. If `device` is of type _screen_:
  //    1. Send a `RequestTimestamp` to the SFU.
  //    2. Let `context` be the corresponding `Timestamp` response.
  // 5. Send the `CaptureState` message for the `device` with the provided
  //    `context`.
  //
  // When receiving this message:
  //
  // 1. Let `device` be the device of the sender whose state has been updated.
  // 2. If `device` was turned _off_ and the user is subscribed to the given
  //    `device`'s feed:
  //    1. Stop displaying the corresponding media feed in the UI.
  //    2. Pause the corresponding media track.
  //    3. If `device` is `Microphone`, no further action is necessary.
  //    4. If `device` is `Camera`, send a `ParticipantCamera.Unsubscribe`
  //       message to the SFU.
  //    5. If `device` is `Screen`, send a `ParticipantScreen.Unsubscribe`
  //       message to the SFU.
  // 3. If `device` was turned _on_ and the user is not subscribed to the given
  //    `device`'s feed:
  //    1. Resume the corresponding media track.
  //    2. Start displaying the corresponding media feed in the UI.
  //    3. If `device` is `Microphone`, no further action is necessary.
  //    4. If `device` is `Camera`, send a `ParticipantCamera.Subscribe` message
  //       to the SFU.
  //    5. If `device` is `Screen`:
  //       1. Send a `ParticipantScreen.Subscribe` message to the SFU.
  //       2. If `device`'s `started_at` timestamp is the most recent timestamp
  //          from all currently active screen shares of all participants, set
  //          the focus to this participant's screen.
  message CaptureState {
    // Capture state of the microphone.
    message Microphone {
      oneof state {
        common.Unit on = 1;
        common.Unit off = 2;
      }
    }

    // Capture state of the camera.
    message Camera {
      oneof state {
        common.Unit on = 1;
        common.Unit off = 2;
      }
    }

    // Capture state of the screen.
    message Screen {
      message On {
        // UNIX-ish timestamp in milliseconds retrieved from the SFU for when
        // the screen share was initiated.
        uint64 started_at = 1;
      }

      oneof state {
        On on = 1;
        common.Unit off = 2;
      }
    }

    oneof state {
      Microphone microphone = 1;
      Camera camera = 2;
      Screen screen = 3;
    }
  }

  // Signals that a participant is currently on hold / temporarily away.
  //
  // When creating this message:
  //
  // 1. Send a `CaptureState` message for each capture device. Follow the
  //    creation steps of that message.
  // 2. Send the `HoldState` message.
  //
  // When receiving this message:
  //
  // 1. Apply the _hold_ state in the UI for the participant.
  // 2. Pause any video-based media tracks of the participant.
  // 3. If subscribed to the participant's camera feed, send a
  //    `ParticipantCamera.Unsubscribe` message to the SFU.
  // 4. If subscribed to the participant's screen feed, send a
  //    `ParticipantScreen.Unsubscribe` message to the SFU.
  message HoldState {}
}
