4
4
5
5
/* eslint-disable require-jsdoc */
6
6
/* global Promise, Map, WebTransport, WebTransportBidirectionalStream,
7
- Uint8Array, Uint32Array, TextEncoder, Worker, MediaStreamTrackProcessor */
7
+ Uint8Array, Uint32Array, TextEncoder, Worker, MediaStreamTrackProcessor,
8
+ MediaStreamTrackGenerator, proto */
8
9
9
10
'use strict' ;
10
11
@@ -44,6 +45,10 @@ export class QuicConnection extends EventDispatcher {
44
45
this . _transportId = this . _token . transportId ;
45
46
this . _initReceiveStreamReader ( ) ;
46
47
this . _worker = new Worker ( workerDir + '/media-worker.js' , { type : 'module' } ) ;
48
+ // Key is subscription ID, value is a MediaStreamTrackGenerator writer.
49
+ this . _mstVideoGeneratorWriters = new Map ( ) ;
50
+ this . _initRtpModule ( ) ;
51
+ this . _initDatagramReader ( ) ;
47
52
}
48
53
49
54
/**
@@ -77,15 +82,18 @@ export class QuicConnection extends EventDispatcher {
77
82
await this . _authenticate ( this . _tokenString ) ;
78
83
}
79
84
85
+ _initRtpModule ( ) {
86
+ this . _worker . postMessage ( [ 'init-rtp' ] ) ;
87
+ }
88
+
80
89
async _initReceiveStreamReader ( ) {
81
90
const receiveStreamReader =
82
91
this . _quicTransport . incomingBidirectionalStreams . getReader ( ) ;
83
- Logger . info ( 'Reader: ' + receiveStreamReader ) ;
84
92
let receivingDone = false ;
85
93
while ( ! receivingDone ) {
86
94
const { value : receiveStream , done : readingReceiveStreamsDone } =
87
95
await receiveStreamReader . read ( ) ;
88
- Logger . info ( 'New stream received' ) ;
96
+ Logger . debug ( 'New stream received. ' ) ;
89
97
const subscriptionIdBytes = new Uint8Array ( uuidByteLength ) ;
90
98
let subscriptionIdBytesOffset = 0 ;
91
99
const trackIdBytes = new Uint8Array ( uuidByteLength ) ;
@@ -173,6 +181,19 @@ export class QuicConnection extends EventDispatcher {
173
181
}
174
182
}
175
183
184
+ async _initDatagramReader ( ) {
185
+ const datagramReader = this . _quicTransport . datagrams . readable . getReader ( ) ;
186
+ let receivingDone = false ;
187
+ while ( ! receivingDone ) {
188
+ const { value : datagram , done : readingDatagramsDone } =
189
+ await datagramReader . read ( ) ;
190
+ this . _worker . postMessage ( [ 'rtp-packet' , datagram ] ) ;
191
+ if ( readingDatagramsDone ) {
192
+ receivingDone = true ;
193
+ }
194
+ }
195
+ }
196
+
176
197
_createSubscription ( id , receiveStream ) {
177
198
// TODO: Incomplete subscription.
178
199
const subscription = new Subscription ( id , ( ) => {
@@ -207,6 +228,95 @@ export class QuicConnection extends EventDispatcher {
207
228
return quicStream ;
208
229
}
209
230
231
+ async bindFeedbackReader ( stream , publicationId ) {
232
+ // The receiver side of a publication stream starts with a UUID of
233
+ // publication ID, then each feedback message has a 4 bytes header indicates
234
+ // its length, and followed by protobuf encoded body.
235
+ const feedbackChunkReader = stream . readable . getReader ( ) ;
236
+ let feedbackChunksDone = false ;
237
+ let publicationIdOffset = 0 ;
238
+ const headerSize = 4 ;
239
+ const header = new Uint8Array ( headerSize ) ;
240
+ let headerOffset = 0 ;
241
+ let bodySize = 0 ;
242
+ let bodyOffset = 0 ;
243
+ let bodyBytes ;
244
+ while ( ! feedbackChunksDone ) {
245
+ let valueOffset = 0 ;
246
+ const { value, done} = await feedbackChunkReader . read ( ) ;
247
+ Logger . debug ( value ) ;
248
+ while ( valueOffset < value . byteLength ) {
249
+ if ( publicationIdOffset < uuidByteLength ) {
250
+ // TODO: Check publication ID matches. For now, we just skip this ID.
251
+ const readLength =
252
+ Math . min ( uuidByteLength - publicationIdOffset , value . byteLength ) ;
253
+ valueOffset += readLength ;
254
+ publicationIdOffset += readLength ;
255
+ }
256
+ if ( headerOffset < headerSize ) {
257
+ // Read header.
258
+ const copyLength = Math . min (
259
+ headerSize - headerOffset , value . byteLength - valueOffset ) ;
260
+ if ( copyLength === 0 ) {
261
+ continue ;
262
+ }
263
+ header . set (
264
+ value . subarray ( valueOffset , valueOffset + copyLength ) ,
265
+ headerOffset ) ;
266
+ headerOffset += copyLength ;
267
+ valueOffset += copyLength ;
268
+ if ( headerOffset < headerSize ) {
269
+ continue ;
270
+ }
271
+ bodySize = 0 ;
272
+ bodyOffset = 0 ;
273
+ for ( let i = 0 ; i < headerSize ; i ++ ) {
274
+ bodySize += ( header [ i ] << ( ( headerSize - 1 - i ) * 8 ) ) ;
275
+ }
276
+ bodyBytes = new Uint8Array ( bodySize ) ;
277
+ Logger . debug ( 'Body size ' + bodySize ) ;
278
+ }
279
+ if ( bodyOffset < bodySize ) {
280
+ const copyLength =
281
+ Math . min ( bodySize - bodyOffset , value . byteLength - valueOffset ) ;
282
+ if ( copyLength === 0 ) {
283
+ continue ;
284
+ }
285
+ Logger . debug ( 'Bytes for body: ' + copyLength ) ;
286
+ bodyBytes . set (
287
+ value . subarray ( valueOffset , valueOffset + copyLength ) ,
288
+ bodyOffset ) ;
289
+ bodyOffset += copyLength ;
290
+ valueOffset += copyLength ;
291
+ if ( valueOffset < bodySize ) {
292
+ continue ;
293
+ }
294
+ // Decode body.
295
+ const feedback =
296
+ proto . owt . protobuf . Feedback . deserializeBinary ( bodyBytes ) ;
297
+ this . handleFeedback ( feedback , publicationId ) ;
298
+ }
299
+ }
300
+ if ( done ) {
301
+ feedbackChunksDone = true ;
302
+ break ;
303
+ }
304
+ }
305
+ }
306
+
307
+ async handleFeedback ( feedback , publicationId ) {
308
+ Logger . debug (
309
+ 'Key frame request type: ' +
310
+ proto . owt . protobuf . Feedback . Type . KEY_FRAME_REQUEST ) ;
311
+ if ( feedback . getType ( ) ===
312
+ proto . owt . protobuf . Feedback . Type . KEY_FRAME_REQUEST ) {
313
+ this . _worker . postMessage (
314
+ [ 'rtcp-feedback' , [ 'key-frame-request' , publicationId ] ] ) ;
315
+ } else {
316
+ Logger . warning ( 'Unrecognized feedback type ' + feedback . getType ( ) ) ;
317
+ }
318
+ }
319
+
210
320
async publish ( stream , options ) {
211
321
// TODO: Avoid a stream to be published twice. The first 16 bit data send to
212
322
// server must be it's publication ID.
@@ -225,6 +335,7 @@ export class QuicConnection extends EventDispatcher {
225
335
for ( const track of stream . stream . getTracks ( ) ) {
226
336
const quicStream =
227
337
await this . _quicTransport . createBidirectionalStream ( ) ;
338
+ this . bindFeedbackReader ( quicStream , publicationId ) ;
228
339
this . _quicMediaStreamTracks . set ( track . id , quicStream ) ;
229
340
quicStreams . push ( quicStream ) ;
230
341
}
@@ -262,6 +373,7 @@ export class QuicConnection extends EventDispatcher {
262
373
[
263
374
'media-sender' ,
264
375
[
376
+ publicationId ,
265
377
track . id ,
266
378
track . kind ,
267
379
processor . readable ,
@@ -317,12 +429,12 @@ export class QuicConnection extends EventDispatcher {
317
429
if ( typeof options !== 'object' ) {
318
430
return Promise . reject ( new TypeError ( 'Options should be an object.' ) ) ;
319
431
}
320
- // if (options.audio === undefined) {
321
- // options.audio = !!stream.settings.audio;
322
- // }
323
- // if (options.video === undefined) {
324
- // options.video = !!stream.settings.video;
325
- // }
432
+ if ( options . audio === undefined ) {
433
+ options . audio = ! ! stream . settings . audio ;
434
+ }
435
+ if ( options . video === undefined ) {
436
+ options . video = ! ! stream . settings . video ;
437
+ }
326
438
let mediaOptions ;
327
439
let dataOptions ;
328
440
if ( options . audio || options . video ) {
@@ -375,19 +487,38 @@ export class QuicConnection extends EventDispatcher {
375
487
} )
376
488
. then ( ( data ) => {
377
489
this . _subscribeOptions . set ( data . id , options ) ;
378
- Logger . debug ( 'Subscribe info is set.' ) ;
379
- if ( this . _quicDataStreams . has ( data . id ) ) {
380
- // QUIC stream created before signaling returns.
381
- // TODO: Update subscription to accept list of QUIC streams.
382
- const subscription = this . _createSubscription (
383
- data . id , this . _quicDataStreams . get ( data . id ) [ 0 ] ) ;
384
- resolve ( subscription ) ;
490
+ if ( dataOptions ) {
491
+ // A WebTransport stream is associated with a subscription for
492
+ // data.
493
+ if ( this . _quicDataStreams . has ( data . id ) ) {
494
+ // QUIC stream created before signaling returns.
495
+ // TODO: Update subscription to accept list of QUIC streams.
496
+ const subscription = this . _createSubscription (
497
+ data . id , this . _quicDataStreams . get ( data . id ) [ 0 ] ) ;
498
+ resolve ( subscription ) ;
499
+ } else {
500
+ this . _quicDataStreams . set ( data . id , null ) ;
501
+ // QUIC stream is not created yet, resolve promise after getting
502
+ // QUIC stream.
503
+ this . _subscribePromises . set (
504
+ data . id , { resolve : resolve , reject : reject } ) ;
505
+ }
385
506
} else {
386
- this . _quicDataStreams . set ( data . id , null ) ;
387
- // QUIC stream is not created yet, resolve promise after getting
388
- // QUIC stream.
389
- this . _subscribePromises . set (
390
- data . id , { resolve : resolve , reject : reject } ) ;
507
+ // A MediaStream is associated with a subscription for media.
508
+ // Media packets are received over WebTransport datagram.
509
+ const generators = [ ] ;
510
+ for ( const track of mediaOptions ) {
511
+ const generator =
512
+ new MediaStreamTrackGenerator ( { kind : track . type } ) ;
513
+ generators . push ( generator ) ;
514
+ // TODO: Update key with the correct SSRC.
515
+ this . _mstVideoGeneratorWriters . set (
516
+ '0' , generator . writable . getWriter ( ) ) ;
517
+ }
518
+ const mediaStream = new MediaStream ( generators ) ;
519
+ const subscription =
520
+ this . _createSubscription ( data . id , mediaStream ) ;
521
+ resolve ( subscription ) ;
391
522
}
392
523
if ( this . _subscriptionInfoReady . has ( data . id ) ) {
393
524
this . _subscriptionInfoReady . get ( data . id ) ( ) ;
@@ -454,4 +585,18 @@ export class QuicConnection extends EventDispatcher {
454
585
datagramReader ( ) {
455
586
return this . _quicTransport . datagrams . readable . getReader ( ) ;
456
587
}
588
+
589
+ initHandlersForWorker ( ) {
590
+ this . _worker . onmessage = ( ( e ) => {
591
+ const [ command , args ] = e . data ;
592
+ switch ( command ) {
593
+ case 'video-frame' :
594
+ // TODO: Use actual subscription ID.
595
+ this . _mstVideoGeneratorWriters . get ( '0' ) . getWriter . write ( args ) ;
596
+ break ;
597
+ default :
598
+ Logger . warn ( 'Unrecognized command ' + command ) ;
599
+ }
600
+ } ) ;
601
+ }
457
602
}
0 commit comments