From 28098a383c33e402c5bc5b4a271c2f4720fd9a29 Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Tue, 20 Oct 2026 09:50:45 +0200 Subject: [PATCH 1/8] fix: arbitrate a pairing connection that adds `'playback'` --- README.md | 2 +- connection.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 91c2de0..ed56c9a 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ When a new server connects, the client lets the handshake complete before applyi - A [pairing attempt](#entering-and-leaving-pairing) is not displaced by an incoming `'playback'` or `'pairing'` connection. - When both the current holder and the incoming connection have empty `activities`, the incoming is admitted only if its `server_id` matches the last-playback server (and the existing one's does not); otherwise the existing is kept. -- A client MAY admit one incoming `'pairing'` connection alongside the admitted `'playback'` connection, holding both. A client that does not admit both connections rejects the incoming with [`pair/abort`](#client--server-pairabort) reason `concurrent_attempt` and closes the connection. While both are held, further incoming connections are arbitrated against the `'playback'` holder. When a later `server/activate` drops `'pairing'` from that connection's `activities`, the client arbitrates it against the `'playback'` holder as if it were incoming. +- A client MAY admit one incoming `'pairing'` connection alongside the admitted `'playback'` connection, holding both. A client that does not admit both connections rejects the incoming with [`pair/abort`](#client--server-pairabort) reason `concurrent_attempt` and closes the connection. While both are held, further incoming connections are arbitrated against the `'playback'` holder. When a later `server/activate` drops `'pairing'` from that connection's `activities` or adds `'playback'` to them, the client arbitrates it against the `'playback'` holder as if it were incoming. Subsequent `server/activate` updates do not otherwise trigger arbitration, even when a connection escalates its activities. A provisional connection that has not sent `server/activate` within 30 seconds is dropped. Clients MAY cap how many provisional connections they hold at once, rejecting further incoming connections as if they were lower priority. diff --git a/connection.md b/connection.md index b150ad5..a072661 100644 --- a/connection.md +++ b/connection.md @@ -35,7 +35,7 @@ When a new server connects, the client lets the handshake complete before applyi - A [pairing attempt](pairing.md#entering-and-leaving-pairing) is not displaced by an incoming `'playback'` or `'pairing'` connection. - When both the current holder and the incoming connection have empty `activities`, the incoming is admitted only if its `server_id` matches the last-playback server (and the existing one's does not); otherwise the existing is kept. -- A client MAY admit one incoming `'pairing'` connection alongside the admitted `'playback'` connection, holding both. A client that does not admit both connections rejects the incoming with [`pair/abort`](pairing.md#client--server-pairabort) reason `concurrent_attempt` and closes the connection. While both are held, further incoming connections are arbitrated against the `'playback'` holder. When a later `server/activate` drops `'pairing'` from that connection's `activities`, the client arbitrates it against the `'playback'` holder as if it were incoming. +- A client MAY admit one incoming `'pairing'` connection alongside the admitted `'playback'` connection, holding both. A client that does not admit both connections rejects the incoming with [`pair/abort`](pairing.md#client--server-pairabort) reason `concurrent_attempt` and closes the connection. While both are held, further incoming connections are arbitrated against the `'playback'` holder. When a later `server/activate` drops `'pairing'` from that connection's `activities` or adds `'playback'` to them, the client arbitrates it against the `'playback'` holder as if it were incoming. Subsequent `server/activate` updates do not otherwise trigger arbitration, even when a connection escalates its activities. A provisional connection that has not sent `server/activate` within 30 seconds is dropped. Clients MAY cap how many provisional connections they hold at once, rejecting further incoming connections as if they were lower priority. From 2e0e6ec709b16a4ff759a5128557730d993129b8 Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Tue, 20 Oct 2026 09:57:07 +0200 Subject: [PATCH 2/8] fix: reject a second pairing connection while playback pairs --- README.md | 2 +- connection.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ed56c9a..0173214 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ When a new server connects, the client lets the handshake complete before applyi - A [pairing attempt](#entering-and-leaving-pairing) is not displaced by an incoming `'playback'` or `'pairing'` connection. - When both the current holder and the incoming connection have empty `activities`, the incoming is admitted only if its `server_id` matches the last-playback server (and the existing one's does not); otherwise the existing is kept. -- A client MAY admit one incoming `'pairing'` connection alongside the admitted `'playback'` connection, holding both. A client that does not admit both connections rejects the incoming with [`pair/abort`](#client--server-pairabort) reason `concurrent_attempt` and closes the connection. While both are held, further incoming connections are arbitrated against the `'playback'` holder. When a later `server/activate` drops `'pairing'` from that connection's `activities` or adds `'playback'` to them, the client arbitrates it against the `'playback'` holder as if it were incoming. +- A client MAY admit one incoming `'pairing'` connection alongside an admitted connection whose `activities` are exactly `['playback']`, holding both. A client that does not admit both connections rejects the incoming with [`pair/abort`](#client--server-pairabort) reason `concurrent_attempt` and closes the connection. While both are held, further incoming connections are arbitrated against the `'playback'` holder. When a later `server/activate` drops `'pairing'` from that connection's `activities` or adds `'playback'` to them, the client arbitrates it against the `'playback'` holder as if it were incoming. Subsequent `server/activate` updates do not otherwise trigger arbitration, even when a connection escalates its activities. A provisional connection that has not sent `server/activate` within 30 seconds is dropped. Clients MAY cap how many provisional connections they hold at once, rejecting further incoming connections as if they were lower priority. diff --git a/connection.md b/connection.md index a072661..4d7c76f 100644 --- a/connection.md +++ b/connection.md @@ -35,7 +35,7 @@ When a new server connects, the client lets the handshake complete before applyi - A [pairing attempt](pairing.md#entering-and-leaving-pairing) is not displaced by an incoming `'playback'` or `'pairing'` connection. - When both the current holder and the incoming connection have empty `activities`, the incoming is admitted only if its `server_id` matches the last-playback server (and the existing one's does not); otherwise the existing is kept. -- A client MAY admit one incoming `'pairing'` connection alongside the admitted `'playback'` connection, holding both. A client that does not admit both connections rejects the incoming with [`pair/abort`](pairing.md#client--server-pairabort) reason `concurrent_attempt` and closes the connection. While both are held, further incoming connections are arbitrated against the `'playback'` holder. When a later `server/activate` drops `'pairing'` from that connection's `activities` or adds `'playback'` to them, the client arbitrates it against the `'playback'` holder as if it were incoming. +- A client MAY admit one incoming `'pairing'` connection alongside an admitted connection whose `activities` are exactly `['playback']`, holding both. A client that does not admit both connections rejects the incoming with [`pair/abort`](pairing.md#client--server-pairabort) reason `concurrent_attempt` and closes the connection. While both are held, further incoming connections are arbitrated against the `'playback'` holder. When a later `server/activate` drops `'pairing'` from that connection's `activities` or adds `'playback'` to them, the client arbitrates it against the `'playback'` holder as if it were incoming. Subsequent `server/activate` updates do not otherwise trigger arbitration, even when a connection escalates its activities. A provisional connection that has not sent `server/activate` within 30 seconds is dropped. Clients MAY cap how many provisional connections they hold at once, rejecting further incoming connections as if they were lower priority. From 47601c75496943bd1ab39f017a6d7c8278682c34 Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Tue, 20 Oct 2026 09:57:19 +0200 Subject: [PATCH 3/8] fix: arbitrate against the held pairing connection --- README.md | 2 +- connection.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0173214..1dfb94e 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ When a new server connects, the client lets the handshake complete before applyi - A [pairing attempt](#entering-and-leaving-pairing) is not displaced by an incoming `'playback'` or `'pairing'` connection. - When both the current holder and the incoming connection have empty `activities`, the incoming is admitted only if its `server_id` matches the last-playback server (and the existing one's does not); otherwise the existing is kept. -- A client MAY admit one incoming `'pairing'` connection alongside an admitted connection whose `activities` are exactly `['playback']`, holding both. A client that does not admit both connections rejects the incoming with [`pair/abort`](#client--server-pairabort) reason `concurrent_attempt` and closes the connection. While both are held, further incoming connections are arbitrated against the `'playback'` holder. When a later `server/activate` drops `'pairing'` from that connection's `activities` or adds `'playback'` to them, the client arbitrates it against the `'playback'` holder as if it were incoming. +- A client MAY admit one incoming `'pairing'` connection alongside an admitted connection whose `activities` are exactly `['playback']`, holding both. A client that does not admit both connections rejects the incoming with [`pair/abort`](#client--server-pairabort) reason `concurrent_attempt` and closes the connection. While both are held, this exception does not apply again: further incoming connections are arbitrated against the `'playback'` holder, and those that declare `'pairing'` also against the held pairing connection, admitted only if accepted against both. When a later `server/activate` drops `'pairing'` from that connection's `activities` or adds `'playback'` to them, the client arbitrates it against the `'playback'` holder as if it were incoming. When a later `server/activate` drops `'playback'` from the `'playback'` holder's `activities` or adds `'pairing'` to them, the client arbitrates it against the held pairing connection as if it were incoming. Subsequent `server/activate` updates do not otherwise trigger arbitration, even when a connection escalates its activities. A provisional connection that has not sent `server/activate` within 30 seconds is dropped. Clients MAY cap how many provisional connections they hold at once, rejecting further incoming connections as if they were lower priority. diff --git a/connection.md b/connection.md index 4d7c76f..5062553 100644 --- a/connection.md +++ b/connection.md @@ -35,7 +35,7 @@ When a new server connects, the client lets the handshake complete before applyi - A [pairing attempt](pairing.md#entering-and-leaving-pairing) is not displaced by an incoming `'playback'` or `'pairing'` connection. - When both the current holder and the incoming connection have empty `activities`, the incoming is admitted only if its `server_id` matches the last-playback server (and the existing one's does not); otherwise the existing is kept. -- A client MAY admit one incoming `'pairing'` connection alongside an admitted connection whose `activities` are exactly `['playback']`, holding both. A client that does not admit both connections rejects the incoming with [`pair/abort`](pairing.md#client--server-pairabort) reason `concurrent_attempt` and closes the connection. While both are held, further incoming connections are arbitrated against the `'playback'` holder. When a later `server/activate` drops `'pairing'` from that connection's `activities` or adds `'playback'` to them, the client arbitrates it against the `'playback'` holder as if it were incoming. +- A client MAY admit one incoming `'pairing'` connection alongside an admitted connection whose `activities` are exactly `['playback']`, holding both. A client that does not admit both connections rejects the incoming with [`pair/abort`](pairing.md#client--server-pairabort) reason `concurrent_attempt` and closes the connection. While both are held, this exception does not apply again: further incoming connections are arbitrated against the `'playback'` holder, and those that declare `'pairing'` also against the held pairing connection, admitted only if accepted against both. When a later `server/activate` drops `'pairing'` from that connection's `activities` or adds `'playback'` to them, the client arbitrates it against the `'playback'` holder as if it were incoming. When a later `server/activate` drops `'playback'` from the `'playback'` holder's `activities` or adds `'pairing'` to them, the client arbitrates it against the held pairing connection as if it were incoming. Subsequent `server/activate` updates do not otherwise trigger arbitration, even when a connection escalates its activities. A provisional connection that has not sent `server/activate` within 30 seconds is dropped. Clients MAY cap how many provisional connections they hold at once, rejecting further incoming connections as if they were lower priority. From 19b620ce04fff704ded0eb0740323097e2d76238 Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Mon, 19 Oct 2026 16:04:09 +0200 Subject: [PATCH 4/8] fix: require `'playback'` for player and visualizer streams --- README.md | 4 ++-- messaging.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1dfb94e..b9ac5d3 100644 --- a/README.md +++ b/README.md @@ -582,7 +582,7 @@ Servers normally activate the client's [preferred](#priority-and-activation) ver Role removal includes explicit removal from `active_roles`, implicit removal when the connection is no longer playback-capable, and replacement of an active role version. -Before a `server/activate` removes a server-to-client stream role (`player`, `artwork`, `visualizer`, or an application-specific role with such a stream), the server MUST send [`stream/end`](#server--client-streamend) for that role if its stream is active. If the first activation after a [re-handshake](#re-handshake) will remove such a role, the server MUST send any required `stream/end` before starting the re-handshake. +Before a `server/activate` removes a server-to-client stream role (`player`, `artwork`, `visualizer`, or an application-specific role with such a stream), the server MUST send [`stream/end`](#server--client-streamend) for that role if its stream is active. Before a `server/activate` drops `'playback'` from `activities`, the server MUST send `stream/end` for the `player` and `visualizer` streams if they are active. If the first activation after a [re-handshake](#re-handshake) will remove such a role or drop `'playback'`, the server MUST send any required `stream/end` before starting the re-handshake. When applying a `server/activate`, the client MUST: @@ -716,7 +716,7 @@ Starts a stream for one or more roles. If sent for a role that already has an ac [Application-specific roles](#application-specific-roles) MAY also include objects in this message (keys starting with `_`). -The server MUST NOT send `stream/start` unless the latest [`client/state`](#client--server-clientstate) it has received reports `available: true`. +The server MUST NOT send `stream/start` unless the latest [`client/state`](#client--server-clientstate) it has received reports `available: true`, and MUST NOT include the `player` or `visualizer` object unless `'playback'` is in the connection's `activities`. Each role's stream configuration is derived from what the client reports about itself: the role's support object in [`client/hello`](#client--server-clienthello) for constant capabilities, and the role's [`client/state`](#client--server-clientstate) object for the stream-configuration fields the client may change during the connection (each role may define both). After a role that defines a `client/state` object is added or re-added to `active_roles`, the server MUST wait for the [`client/state`](#client--server-clientstate) update the activation requires before starting that role's stream, so it does not start from stale state. When a `client/state` changes a role's stream-configuration fields while a stream is active for that role, the server re-derives the stream configuration and, if it changed, sends a `stream/start` with the new configuration. When no stream is active for the role, the server MUST NOT start one in response; the updated state applies to the next stream it starts for that role. diff --git a/messaging.md b/messaging.md index a864cea..095cd2d 100644 --- a/messaging.md +++ b/messaging.md @@ -264,7 +264,7 @@ Servers normally activate the client's [preferred](README.md#priority-and-activa Role removal includes explicit removal from `active_roles`, implicit removal when the connection is no longer playback-capable, and replacement of an active role version. -Before a `server/activate` removes a server-to-client stream role (`player`, `artwork`, `visualizer`, or an application-specific role with such a stream), the server MUST send [`stream/end`](#server--client-streamend) for that role if its stream is active. If the first activation after a [re-handshake](connection.md#re-handshake) will remove such a role, the server MUST send any required `stream/end` before starting the re-handshake. +Before a `server/activate` removes a server-to-client stream role (`player`, `artwork`, `visualizer`, or an application-specific role with such a stream), the server MUST send [`stream/end`](#server--client-streamend) for that role if its stream is active. Before a `server/activate` drops `'playback'` from `activities`, the server MUST send `stream/end` for the `player` and `visualizer` streams if they are active. If the first activation after a [re-handshake](connection.md#re-handshake) will remove such a role or drop `'playback'`, the server MUST send any required `stream/end` before starting the re-handshake. When applying a `server/activate`, the client MUST: @@ -398,7 +398,7 @@ Starts a stream for one or more roles. If sent for a role that already has an ac [Application-specific roles](README.md#application-specific-roles) MAY also include objects in this message (keys starting with `_`). -The server MUST NOT send `stream/start` unless the latest [`client/state`](#client--server-clientstate) it has received reports `available: true`. +The server MUST NOT send `stream/start` unless the latest [`client/state`](#client--server-clientstate) it has received reports `available: true`, and MUST NOT include the `player` or `visualizer` object unless `'playback'` is in the connection's `activities`. Each role's stream configuration is derived from what the client reports about itself: the role's support object in [`client/hello`](#client--server-clienthello) for constant capabilities, and the role's [`client/state`](#client--server-clientstate) object for the stream-configuration fields the client may change during the connection (each role may define both). After a role that defines a `client/state` object is added or re-added to `active_roles`, the server MUST wait for the [`client/state`](#client--server-clientstate) update the activation requires before starting that role's stream, so it does not start from stale state. When a `client/state` changes a role's stream-configuration fields while a stream is active for that role, the server re-derives the stream configuration and, if it changed, sends a `stream/start` with the new configuration. When no stream is active for the role, the server MUST NOT start one in response; the updated state applies to the next stream it starts for that role. From d9dcafb984cb612adf298c830eada536cd764ef6 Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Tue, 20 Oct 2026 09:22:34 +0200 Subject: [PATCH 5/8] fix: require `'playback'` for the source `start` command --- README.md | 6 +++--- roles/source/v1.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b9ac5d3..04045c6 100644 --- a/README.md +++ b/README.md @@ -1454,7 +1454,7 @@ The `source` object in [`server/command`](#server--client-servercommand) has thi A `start` received while a start authorization is pending or the input stream is open has no effect: it MUST NOT restart the stream or authorize a later reopening. A `stop` received with no pending start authorization and no open input stream is ignored. -The server MUST NOT send `start` unless the source role is active, it has received the source object in the [`client/state`](#client--server-clientstate) update required by that activation, and the latest `client/state` it received reports `available: true`. +The server MUST NOT send `start` unless the source role is active, `'playback'` is in the connection's [`activities`](#server--client-serveractivate), it has received the source object in the [`client/state`](#client--server-clientstate) update required by that activation, and the latest `client/state` it received reports `available: true`. A client MUST ignore `start` received while it is unavailable. Becoming available again does not resume streaming without a new `start`. @@ -1468,7 +1468,7 @@ Streaming state is per-connection: a previously sent `start` does not survive re A source that supports line sensing reports `signal` in [`client/state`](#client--server-clientstate). The server MAY use it as a hint for when to send `command: "start"` or `command: "stop"`, but the decision is server policy. -When the client receives a [`server/activate`](#server--client-serveractivate) removing `source` from `active_roles`, it MUST clear any pending start authorization, stop sending chunks, and send `client-stream/end` if its input stream is open. After reactivation, the client MUST NOT resume streaming until it receives a new `start`. +When the client receives a [`server/activate`](#server--client-serveractivate) removing `source` from `active_roles` or dropping `'playback'` from `activities`, it MUST clear any pending start authorization, stop sending chunks, and send `client-stream/end` if its input stream is open. After reactivation, the client MUST NOT resume streaming until it receives a new `start`. A source that becomes unavailable MUST clear any pending start authorization. If its input stream is open, it MUST stop sending source audio chunks and send `client-stream/end` before reporting [`available: false`](#external-source-handling) in `client/state`. The server treats the transition as an implicit `stop`. @@ -1503,7 +1503,7 @@ The client ends the current input stream. After sending this message, the client Receiving a source audio message without an open input stream, or while the latest [`client/state`](#client--server-clientstate) received by the server reports `available: false`, is a protocol error: the server MUST close the connection. The client's stream lifecycle messages, audio chunks, and availability updates are delivered in send order on the same connection, so network delay cannot move earlier chunks past `client-stream/end` or an availability update. -Clients MUST send `client-stream/start` before the first audio chunk. Sending `command: "stop"` or removing the source role does not itself close an input stream at the server. After either action, the server MUST tolerate otherwise valid in-flight responses to a previously sent `start`, including `client-stream/start` and audio chunks, until it receives `client-stream/end`. It MAY discard audio payloads but MUST process `client-stream/start` and `client-stream/end` in received order. +Clients MUST send `client-stream/start` before the first audio chunk. Sending `command: "stop"`, removing the source role, or dropping `'playback'` does not itself close an input stream at the server. After any of these, the server MUST tolerate otherwise valid in-flight responses to a previously sent `start`, including `client-stream/start` and audio chunks, until it receives `client-stream/end`. It MAY discard audio payloads but MUST process `client-stream/start` and `client-stream/end` in received order. - Byte 0: message type `12` (uint8) - Bytes 1-8: timestamp (big-endian int64) - server clock time in microseconds when the first sample was captured diff --git a/roles/source/v1.md b/roles/source/v1.md index cc3829d..5eda0f6 100644 --- a/roles/source/v1.md +++ b/roles/source/v1.md @@ -54,7 +54,7 @@ The `source` object in [`server/command`](../../messaging.md#server--client-serv A `start` received while a start authorization is pending or the input stream is open has no effect: it MUST NOT restart the stream or authorize a later reopening. A `stop` received with no pending start authorization and no open input stream is ignored. -The server MUST NOT send `start` unless the source role is active, it has received the source object in the [`client/state`](../../messaging.md#client--server-clientstate) update required by that activation, and the latest `client/state` it received reports `available: true`. +The server MUST NOT send `start` unless the source role is active, `'playback'` is in the connection's [`activities`](../../messaging.md#server--client-serveractivate), it has received the source object in the [`client/state`](../../messaging.md#client--server-clientstate) update required by that activation, and the latest `client/state` it received reports `available: true`. A client MUST ignore `start` received while it is unavailable. Becoming available again does not resume streaming without a new `start`. @@ -68,7 +68,7 @@ Streaming state is per-connection: a previously sent `start` does not survive re A source that supports line sensing reports `signal` in [`client/state`](../../messaging.md#client--server-clientstate). The server MAY use it as a hint for when to send `command: "start"` or `command: "stop"`, but the decision is server policy. -When the client receives a [`server/activate`](../../messaging.md#server--client-serveractivate) removing `source` from `active_roles`, it MUST clear any pending start authorization, stop sending chunks, and send `client-stream/end` if its input stream is open. After reactivation, the client MUST NOT resume streaming until it receives a new `start`. +When the client receives a [`server/activate`](../../messaging.md#server--client-serveractivate) removing `source` from `active_roles` or dropping `'playback'` from `activities`, it MUST clear any pending start authorization, stop sending chunks, and send `client-stream/end` if its input stream is open. After reactivation, the client MUST NOT resume streaming until it receives a new `start`. A source that becomes unavailable MUST clear any pending start authorization. If its input stream is open, it MUST stop sending source audio chunks and send `client-stream/end` before reporting [`available: false`](../../messaging.md#external-source-handling) in `client/state`. The server treats the transition as an implicit `stop`. @@ -103,7 +103,7 @@ The client ends the current input stream. After sending this message, the client Receiving a source audio message without an open input stream, or while the latest [`client/state`](../../messaging.md#client--server-clientstate) received by the server reports `available: false`, is a protocol error: the server MUST close the connection. The client's stream lifecycle messages, audio chunks, and availability updates are delivered in send order on the same connection, so network delay cannot move earlier chunks past `client-stream/end` or an availability update. -Clients MUST send `client-stream/start` before the first audio chunk. Sending `command: "stop"` or removing the source role does not itself close an input stream at the server. After either action, the server MUST tolerate otherwise valid in-flight responses to a previously sent `start`, including `client-stream/start` and audio chunks, until it receives `client-stream/end`. It MAY discard audio payloads but MUST process `client-stream/start` and `client-stream/end` in received order. +Clients MUST send `client-stream/start` before the first audio chunk. Sending `command: "stop"`, removing the source role, or dropping `'playback'` does not itself close an input stream at the server. After any of these, the server MUST tolerate otherwise valid in-flight responses to a previously sent `start`, including `client-stream/start` and audio chunks, until it receives `client-stream/end`. It MAY discard audio payloads but MUST process `client-stream/start` and `client-stream/end` in received order. - Byte 0: message type `12` (uint8) - Bytes 1-8: timestamp (big-endian int64) - server clock time in microseconds when the first sample was captured From 2ee24f11244c7fb8b2c48644a7bcca52d5633a09 Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Tue, 20 Oct 2026 09:42:55 +0200 Subject: [PATCH 6/8] fix: say when a server declares and drops `'playback'` --- README.md | 2 +- messaging.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 04045c6..863d6a4 100644 --- a/README.md +++ b/README.md @@ -576,7 +576,7 @@ When `'pairing'` is in `activities`, `pairing.method` MUST be `'pairing_psk'` if **Worked example (`pairing_required`).** A Sentinel-keyed connection to a client with unpaired access disabled receives `activities: ['playback']` and `active_roles: ['player@v1']`. Under a hypothetical `unpaired_access: enabled`, `['playback']` would be an allowed set for the Sentinel PSK and the connection would be playback-capable, so the activation would be admissible: the client closes with `'pairing_required'`. -Servers SHOULD declare the minimal set of activities that reflects the connection's current purpose, and drop an activity as soon as that purpose ends. Admission between competing connections is decided by the highest-ranked declared activity (see [Multiple servers](#multiple-servers-server-initiated)), so keeping an unused activity declared would degrade multi-server cooperation. +Servers SHOULD declare the minimal set of activities that reflects the connection's current purpose, and drop an activity as soon as that purpose ends. Admission between competing connections is decided by the highest-ranked declared activity (see [Multiple servers](#multiple-servers-server-initiated)), so keeping an unused activity declared would degrade multi-server cooperation. A server SHOULD declare `'playback'` while the client's group is playing or while it wants the client's `source` to stream, whatever roles are active, and drop it once neither is the case, not at a track change or while the client moves to another playing group. Servers normally activate the client's [preferred](#priority-and-activation) version of each role, but MAY omit a role at their discretion (e.g., based on whether the session is paired, deployment context, or operator policy). Checking `active_roles` is therefore required to determine what the client may actually use on this session. diff --git a/messaging.md b/messaging.md index 095cd2d..f511192 100644 --- a/messaging.md +++ b/messaging.md @@ -258,7 +258,7 @@ When `'pairing'` is in `activities`, `pairing.method` MUST be `'pairing_psk'` if **Worked example (`pairing_required`).** A Sentinel-keyed connection to a client with unpaired access disabled receives `activities: ['playback']` and `active_roles: ['player@v1']`. Under a hypothetical `unpaired_access: enabled`, `['playback']` would be an allowed set for the Sentinel PSK and the connection would be playback-capable, so the activation would be admissible: the client closes with `'pairing_required'`. -Servers SHOULD declare the minimal set of activities that reflects the connection's current purpose, and drop an activity as soon as that purpose ends. Admission between competing connections is decided by the highest-ranked declared activity (see [Multiple servers](connection.md#multiple-servers-server-initiated)), so keeping an unused activity declared would degrade multi-server cooperation. +Servers SHOULD declare the minimal set of activities that reflects the connection's current purpose, and drop an activity as soon as that purpose ends. Admission between competing connections is decided by the highest-ranked declared activity (see [Multiple servers](connection.md#multiple-servers-server-initiated)), so keeping an unused activity declared would degrade multi-server cooperation. A server SHOULD declare `'playback'` while the client's group is playing or while it wants the client's `source` to stream, whatever roles are active, and drop it once neither is the case, not at a track change or while the client moves to another playing group. Servers normally activate the client's [preferred](README.md#priority-and-activation) version of each role, but MAY omit a role at their discretion (e.g., based on whether the session is paired, deployment context, or operator policy). Checking `active_roles` is therefore required to determine what the client may actually use on this session. From 42dcc67a9eec5c5ad175c7301d917912c0fe0bc9 Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Wed, 21 Oct 2026 11:50:48 +0200 Subject: [PATCH 7/8] fix: ignore player commands without `'playback'` while another plays --- README.md | 2 ++ roles/player/v1.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 863d6a4..9fec51f 100644 --- a/README.md +++ b/README.md @@ -1291,6 +1291,8 @@ Request the player to perform an action, e.g., change volume or mute state. The server MUST NOT send a player command to a client before that client has sent its initial [`client/state`](#client--server-clientstate). +While another admitted connection has `'playback'` in its [`activities`](#server--client-serveractivate), the client MUST ignore player commands from a connection without `'playback'`. + ### Server → Client: `stream/start` player object The `player` object in [`stream/start`](#server--client-streamstart) has this structure: diff --git a/roles/player/v1.md b/roles/player/v1.md index 384a831..1118995 100644 --- a/roles/player/v1.md +++ b/roles/player/v1.md @@ -80,6 +80,8 @@ Request the player to perform an action, e.g., change volume or mute state. The server MUST NOT send a player command to a client before that client has sent its initial [`client/state`](../../messaging.md#client--server-clientstate). +While another admitted connection has `'playback'` in its [`activities`](../../messaging.md#server--client-serveractivate), the client MUST ignore player commands from a connection without `'playback'`. + ### Server → Client: `stream/start` player object The `player` object in [`stream/start`](../../messaging.md#server--client-streamstart) has this structure: From 06dfa204e728459f4bfb526b40b86b44337b6cc0 Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Wed, 21 Oct 2026 12:03:32 +0200 Subject: [PATCH 8/8] fix: ignore audio streams and source `start` without `'playback'` --- README.md | 4 +++- messaging.md | 2 ++ roles/source/v1.md | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9fec51f..4c355fa 100644 --- a/README.md +++ b/README.md @@ -718,6 +718,8 @@ Starts a stream for one or more roles. If sent for a role that already has an ac The server MUST NOT send `stream/start` unless the latest [`client/state`](#client--server-clientstate) it has received reports `available: true`, and MUST NOT include the `player` or `visualizer` object unless `'playback'` is in the connection's `activities`. +On a connection without `'playback'`, the client MUST ignore the `player` and `visualizer` objects in `stream/start` and discard player audio and visualization data. + Each role's stream configuration is derived from what the client reports about itself: the role's support object in [`client/hello`](#client--server-clienthello) for constant capabilities, and the role's [`client/state`](#client--server-clientstate) object for the stream-configuration fields the client may change during the connection (each role may define both). After a role that defines a `client/state` object is added or re-added to `active_roles`, the server MUST wait for the [`client/state`](#client--server-clientstate) update the activation requires before starting that role's stream, so it does not start from stale state. When a `client/state` changes a role's stream-configuration fields while a stream is active for that role, the server re-derives the stream configuration and, if it changed, sends a `stream/start` with the new configuration. When no stream is active for the role, the server MUST NOT start one in response; the updated state applies to the next stream it starts for that role. Clients may change their stream-configuration fields to adapt to changing network conditions, CPU constraints, or display requirements. The server maintains separate encoding for each client, allowing heterogeneous device capabilities within the same group. @@ -1458,7 +1460,7 @@ A `start` received while a start authorization is pending or the input stream is The server MUST NOT send `start` unless the source role is active, `'playback'` is in the connection's [`activities`](#server--client-serveractivate), it has received the source object in the [`client/state`](#client--server-clientstate) update required by that activation, and the latest `client/state` it received reports `available: true`. -A client MUST ignore `start` received while it is unavailable. Becoming available again does not resume streaming without a new `start`. +A client MUST ignore `start` received while it is unavailable or while `'playback'` is not in the connection's `activities`. Becoming available again does not resume streaming without a new `start`. #### Default streaming behavior diff --git a/messaging.md b/messaging.md index f511192..3f53440 100644 --- a/messaging.md +++ b/messaging.md @@ -400,6 +400,8 @@ Starts a stream for one or more roles. If sent for a role that already has an ac The server MUST NOT send `stream/start` unless the latest [`client/state`](#client--server-clientstate) it has received reports `available: true`, and MUST NOT include the `player` or `visualizer` object unless `'playback'` is in the connection's `activities`. +On a connection without `'playback'`, the client MUST ignore the `player` and `visualizer` objects in `stream/start` and discard player audio and visualization data. + Each role's stream configuration is derived from what the client reports about itself: the role's support object in [`client/hello`](#client--server-clienthello) for constant capabilities, and the role's [`client/state`](#client--server-clientstate) object for the stream-configuration fields the client may change during the connection (each role may define both). After a role that defines a `client/state` object is added or re-added to `active_roles`, the server MUST wait for the [`client/state`](#client--server-clientstate) update the activation requires before starting that role's stream, so it does not start from stale state. When a `client/state` changes a role's stream-configuration fields while a stream is active for that role, the server re-derives the stream configuration and, if it changed, sends a `stream/start` with the new configuration. When no stream is active for the role, the server MUST NOT start one in response; the updated state applies to the next stream it starts for that role. Clients may change their stream-configuration fields to adapt to changing network conditions, CPU constraints, or display requirements. The server maintains separate encoding for each client, allowing heterogeneous device capabilities within the same group. diff --git a/roles/source/v1.md b/roles/source/v1.md index 5eda0f6..46c2c26 100644 --- a/roles/source/v1.md +++ b/roles/source/v1.md @@ -56,7 +56,7 @@ A `start` received while a start authorization is pending or the input stream is The server MUST NOT send `start` unless the source role is active, `'playback'` is in the connection's [`activities`](../../messaging.md#server--client-serveractivate), it has received the source object in the [`client/state`](../../messaging.md#client--server-clientstate) update required by that activation, and the latest `client/state` it received reports `available: true`. -A client MUST ignore `start` received while it is unavailable. Becoming available again does not resume streaming without a new `start`. +A client MUST ignore `start` received while it is unavailable or while `'playback'` is not in the connection's `activities`. Becoming available again does not resume streaming without a new `start`. #### Default streaming behavior