1
- import { ClientMessage , ServerMessage , TrackInfo } from "./sfu.ts" ;
1
+ import { ClientMessage , MediaConfig , ServerMessage } from "./sfu.ts" ;
2
2
3
- const MAX_DOWNSTREAMS = 9 ;
4
-
5
- type MID = string ;
3
+ const MAX_DOWNSTREAMS = 16 ;
4
+ const LAST_N_AUDIO = 3 ;
6
5
7
6
// Internal Ids
8
7
type ParticipantId = string ;
9
- type TrackId = string ;
10
8
11
- interface Slot {
12
- transceiver : RTCRtpTransceiver ;
13
- track : MediaStreamTrack ;
14
- info ?: TrackInfo ;
9
+ interface VideoSlot {
10
+ trans : RTCRtpTransceiver ;
11
+ participantId ?: ParticipantId ;
15
12
}
16
13
17
- interface ParticipantSlot {
18
- video ?: MID ;
19
- audio ?: MID ;
14
+ interface ParticipantMeta {
15
+ externalParticipantId : string ;
16
+ media ?: MediaConfig ;
20
17
}
21
18
22
19
export interface ClientCoreConfig {
@@ -32,12 +29,13 @@ export class ClientCore {
32
29
#audioSender: RTCRtpTransceiver ;
33
30
#closed: boolean ;
34
31
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 > ;
38
36
39
- onStateChanged = ( state : RTCPeerConnectionState ) => { } ;
40
- onTrack = ( track : RTCPeerConnection ) => { } ;
37
+ onStateChanged = ( state : RTCPeerConnectionState ) => { } ;
38
+ onTrack = ( track : RTCPeerConnection ) => { } ;
41
39
42
40
constructor ( cfg : ClientCoreConfig ) {
43
41
this . #sfuUrl = cfg . sfuUrl ;
@@ -46,9 +44,9 @@ export class ClientCore {
46
44
0 ,
47
45
) ;
48
46
this . #closed = false ;
49
- this . #slots = { } ;
50
- this . #availableTracks = { } ;
51
- this . #participantSlots = [ ] ;
47
+ this . #videoSlots = [ ] ;
48
+ this . #audioSlots = [ ] ;
49
+ this . #participants = { } ;
52
50
53
51
this . #pc = new RTCPeerConnection ( ) ;
54
52
this . #pc. onconnectionstatechange = ( ) => {
@@ -66,48 +64,6 @@ export class ClientCore {
66
64
}
67
65
} ;
68
66
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
-
111
67
// SFU RPC DataChannel
112
68
this . #rpc = this . #pc. createDataChannel ( "pulsebeam::rpc" ) ;
113
69
this . #rpc. binaryType = "arraybuffer" ;
@@ -116,22 +72,39 @@ export class ClientCore {
116
72
const serverMessage = ServerMessage . fromBinary (
117
73
new Uint8Array ( event . data as ArrayBuffer ) ,
118
74
) ;
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 ) {
122
78
console . warn ( "Received SFU message with undefined payload kind." ) ;
123
79
return ;
124
80
}
125
81
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
+ }
130
90
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
+ }
132
106
break ;
133
107
}
134
-
135
108
// TODO: implement this
136
109
} catch ( e : any ) {
137
110
this . #close( `Error processing SFU RPC message: ${ e } ` ) ;
@@ -152,13 +125,14 @@ export class ClientCore {
152
125
direction : "sendonly" ,
153
126
} ) ;
154
127
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" , {
158
130
direction : "recvonly" ,
159
131
} ) ;
132
+ }
160
133
161
- this . #pc. addTransceiver ( "audio" , {
134
+ for ( let i = 0 ; i < maxDownstreams ; i ++ ) {
135
+ this . #pc. addTransceiver ( "video" , {
162
136
direction : "recvonly" ,
163
137
} ) ;
164
138
}
@@ -182,6 +156,13 @@ export class ClientCore {
182
156
throw new Error ( errorMessage ) ; // More direct feedback to developer
183
157
}
184
158
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
+
185
166
try {
186
167
const offer = await this . #pc. createOffer ( ) ;
187
168
await this . #pc. setLocalDescription ( offer ) ;
@@ -203,7 +184,25 @@ export class ClientCore {
203
184
type : "answer" ,
204
185
sdp : await response . text ( ) ,
205
186
} ) ;
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
207
206
} catch ( error : any ) {
208
207
this . #close(
209
208
error . message || "Signaling process failed unexpectedly." ,
0 commit comments