Code change:
3.2. <del> The first stream opened is a client-initiated bidirectional stream where the peers exchange SETUP messages (message-setup).  The subsequent streams MAY be either unidirectional or bidirectional. For exchanging content, an application would typically send a unidirectional stream containing a single OBJECT message (message- object), as putting more than one object into one stream may create head-of-line blocking delays.  However, if one object has a hard dependency on another object, putting them on the same stream could be a valid choice. </del> <ins> The first stream opened is a client-initiated bidirectional control stream where the peers exchange SETUP messages (message-setup).  All messages defined in this draft are sent on the control stream after the SETUP message.  Control messages MUST NOT be sent on any other stream, and a peer receiving a control message on a different stream closes the session as a 'Protocol Violation'.  Objects MUST NOT be sent on the control stream, and a peer receiving an Object on the control stream closes the session as a 'Protocol Violation'. This draft only specifies a single use of bidirectional streams. Objects are sent on unidirectional streams.  Because there are no other uses of bidirectional streams, a peer MAY currently close the connection if it receives a second bidirectional stream. The control stream MUST NOT be abruptly closed at the QUIC layer. Doing so results in the session being closed as a 'Protocol Violation'. </ins> 3.3.

Related discussion:
text: Otherwise, a QUIC library would need the ability to reprioritize a stream at a given byte offset. If control messages are always higher priority than data messages (debated), then separate streams are required. I also mentioned that in my implementation, separate threads process control and data messages separately. Data streams are immediately sent to a media decoder thread (WebWorker) while control streams stay in the main thread. This architecture is nice but breaks if control messages and data messages can be interleaved. I think we were in pretty good alignment that there should be control streams. The primary discussion surrounds the scope and number of control streams. control stream per track. control stream per track namespace. control stream per session. I don't think option 1 is viable for ABR, as you want to coalesce the UNSUBSCRIBE / SUBSCRIBE for separate tracks. For example, (near) simultaneously unsubscribe to 480p and subscribe to 720p. If the tracks are truly independent and can not tolerate head-of-line blocking, perhaps they should use a separate namespace instead? I think option 2 is acceptable. The goal is to avoid introducing head-of-line blocking for thousands of unrelated track namespaces, but I think this bleeds into the infamous pooling discussion. If pooling is accomplished via multiple sessions like WebTransport, then option 3 also accomplishes that goal. I'm currently using option 3 since it makes the SETUP marginally handshake easier. Control messages are written immediately after the SETUP message, avoiding the need to buffer messages pending the handshake. It also means you can coordinate subscriptions when tracks are NOT within the same track namespace (ex. ABR across different producers).
Okay so I can't help but start a tiny bit of the POOLING discussion. My application uses Warp for media and Alan's protocol for chat. Let's assume that I want to use the same QUIC connection for both for efficiency reasons. I would like to design my application into two halves, so one part of the code deals with chat and one part deals with media. I don't want a situation where I have to pass control and data messages between threads. It would be really nice if there were two control streams: one to control the chat protocol and one to control the media protocol.