1- import { ClientMessage , ServerMessage , TrackInfo } from "./sfu.ts" ;
1+ import { ClientMessage , MediaConfig , ServerMessage } from "./sfu.ts" ;
22
3- const MAX_DOWNSTREAMS = 9 ;
4-
5- type MID = string ;
3+ const MAX_DOWNSTREAMS = 16 ;
4+ const LAST_N_AUDIO = 3 ;
65
76// Internal Ids
87type ParticipantId = string ;
9- type TrackId = string ;
108
11- interface Slot {
12- transceiver : RTCRtpTransceiver ;
13- track : MediaStreamTrack ;
14- info ?: TrackInfo ;
9+ interface VideoSlot {
10+ trans : RTCRtpTransceiver ;
11+ participantId ?: ParticipantId ;
1512}
1613
17- interface ParticipantSlot {
18- video ?: MID ;
19- audio ?: MID ;
14+ interface ParticipantMeta {
15+ externalParticipantId : string ;
16+ media ?: MediaConfig ;
2017}
2118
2219export interface ClientCoreConfig {
@@ -32,12 +29,13 @@ export class ClientCore {
3229 #audioSender: RTCRtpTransceiver ;
3330 #closed: boolean ;
3431
35- #slots: Record < MID , Slot > ;
36- #participantSlots: ParticipantSlot [ ] ;
37- #availableTracks: Record < ParticipantId , Record < TrackId , TrackInfo > > ;
32+ #videoSlots: VideoSlot [ ] ;
33+ #audioSlots: RTCRtpTransceiver [ ] ;
34+
35+ #participants: Record < ParticipantId , ParticipantMeta > ;
3836
39- onStateChanged = ( state : RTCPeerConnectionState ) => { } ;
40- onTrack = ( track : RTCPeerConnection ) => { } ;
37+ onStateChanged = ( state : RTCPeerConnectionState ) => { } ;
38+ onTrack = ( track : RTCPeerConnection ) => { } ;
4139
4240 constructor ( cfg : ClientCoreConfig ) {
4341 this . #sfuUrl = cfg . sfuUrl ;
@@ -46,9 +44,9 @@ export class ClientCore {
4644 0 ,
4745 ) ;
4846 this . #closed = false ;
49- this . #slots = { } ;
50- this . #availableTracks = { } ;
51- this . #participantSlots = [ ] ;
47+ this . #videoSlots = [ ] ;
48+ this . #audioSlots = [ ] ;
49+ this . #participants = { } ;
5250
5351 this . #pc = new RTCPeerConnection ( ) ;
5452 this . #pc. onconnectionstatechange = ( ) => {
@@ -66,48 +64,6 @@ export class ClientCore {
6664 }
6765 } ;
6866
69- this . #pc. ontrack = ( event : RTCTrackEvent ) => {
70- const mid = event . transceiver ?. mid ;
71- const track = event . track ;
72- const transceiver = event . transceiver ;
73- if ( ! mid || ! track ) {
74- this . #close( "Received track event without MID or track object." ) ;
75- return ;
76- }
77-
78- console . log ( event ) ;
79- this . #slots[ mid ] = {
80- track,
81- transceiver,
82- } ;
83-
84- if ( track . kind === "video" ) {
85- for ( const slot of this . #participantSlots) {
86- if ( ! slot . video ) {
87- slot . video = mid ;
88- return ;
89- }
90- }
91-
92- this . #participantSlots. push ( {
93- video : mid ,
94- } ) ;
95- } else if ( track . kind === "audio" ) {
96- for ( const slot of this . #participantSlots) {
97- if ( ! slot . audio ) {
98- slot . audio = mid ;
99- return ;
100- }
101- }
102-
103- this . #participantSlots. push ( {
104- audio : mid ,
105- } ) ;
106- } else {
107- console . warn ( "unknown track kind, ignoring:" , track . kind ) ;
108- }
109- } ;
110-
11167 // SFU RPC DataChannel
11268 this . #rpc = this . #pc. createDataChannel ( "pulsebeam::rpc" ) ;
11369 this . #rpc. binaryType = "arraybuffer" ;
@@ -116,22 +72,39 @@ export class ClientCore {
11672 const serverMessage = ServerMessage . fromBinary (
11773 new Uint8Array ( event . data as ArrayBuffer ) ,
11874 ) ;
119- const payload = serverMessage . payload ;
120- const payloadKind = payload . oneofKind ;
121- if ( ! payloadKind ) {
75+ const msg = serverMessage . msg ;
76+ const msgKind = msg . oneofKind ;
77+ if ( ! msgKind ) {
12278 console . warn ( "Received SFU message with undefined payload kind." ) ;
12379 return ;
12480 }
12581
126- switch ( payloadKind ) {
127- case "trackPublished" :
128- break ;
129- case "trackUnpublished" :
82+ switch ( msgKind ) {
83+ case "roomSnapshot" :
84+ for ( const participant of msg . roomSnapshot . participants ) {
85+ this . #participants[ participant . participantId ] = {
86+ externalParticipantId : participant . externalParticipantId ,
87+ media : participant . media ,
88+ } ;
89+ }
13090 break ;
131- case "trackSwitched" :
91+ case "streamUpdate" :
92+ if ( msg . streamUpdate . participantStream ) {
93+ const stream = msg . streamUpdate . participantStream ;
94+ if ( stream . participantId in this . #participants) {
95+ const participant = this . #participants[ stream . participantId ] ;
96+ participant . media = stream . media ;
97+ participant . externalParticipantId =
98+ stream . externalParticipantId ;
99+ } else {
100+ this . #participants[ stream . participantId ] = {
101+ externalParticipantId : stream . externalParticipantId ,
102+ media : stream . media ,
103+ } ;
104+ }
105+ }
132106 break ;
133107 }
134-
135108 // TODO: implement this
136109 } catch ( e : any ) {
137110 this . #close( `Error processing SFU RPC message: ${ e } ` ) ;
@@ -152,13 +125,14 @@ export class ClientCore {
152125 direction : "sendonly" ,
153126 } ) ;
154127
155- for ( let i = 0 ; i < maxDownstreams ; i ++ ) {
156- // ontrack will be fired with acknowledgement from the server
157- this . #pc. addTransceiver ( "video" , {
128+ for ( let i = 0 ; i < LAST_N_AUDIO ; i ++ ) {
129+ this . #pc. addTransceiver ( "audio" , {
158130 direction : "recvonly" ,
159131 } ) ;
132+ }
160133
161- this . #pc. addTransceiver ( "audio" , {
134+ for ( let i = 0 ; i < maxDownstreams ; i ++ ) {
135+ this . #pc. addTransceiver ( "video" , {
162136 direction : "recvonly" ,
163137 } ) ;
164138 }
@@ -182,6 +156,13 @@ export class ClientCore {
182156 throw new Error ( errorMessage ) ; // More direct feedback to developer
183157 }
184158
159+ if ( this . #pc. connectionState != "new" ) {
160+ const errorMessage =
161+ "This client instance has been initiated and cannot be reused." ;
162+ console . error ( errorMessage ) ;
163+ throw new Error ( errorMessage ) ; // More direct feedback to developer
164+ }
165+
185166 try {
186167 const offer = await this . #pc. createOffer ( ) ;
187168 await this . #pc. setLocalDescription ( offer ) ;
@@ -203,7 +184,25 @@ export class ClientCore {
203184 type : "answer" ,
204185 sdp : await response . text ( ) ,
205186 } ) ;
206- // Status transitions to "connected" will be handled by onconnectionstatechange and data channel onopen events.
187+
188+ // https://blog.mozilla.org/webrtc/rtcrtptransceiver-explored/
189+ // transceivers order is stable, and mid is only defined after setLocalDescription
190+ const transceivers = this . #pc. getTransceivers ( ) ;
191+ for ( const trans of transceivers ) {
192+ if ( trans . direction === "sendonly" ) {
193+ continue ;
194+ }
195+
196+ if ( trans . receiver . track . kind === "audio" ) {
197+ this . #audioSlots. push ( trans ) ;
198+ } else if ( trans . receiver . track . kind === "video" ) {
199+ this . #videoSlots. push ( {
200+ trans,
201+ } ) ;
202+ }
203+ }
204+
205+ // Status transitions to "connected" will be handled by onconnectionstatechange
207206 } catch ( error : any ) {
208207 this . #close(
209208 error . message || "Signaling process failed unexpectedly." ,
0 commit comments