@@ -66,26 +66,27 @@ class SteeringManifest {
66
66
* https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ section 4.4.6.6.
67
67
* DASH: https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf
68
68
*
69
- * @param {Object } segmentLoader a reference to the mainSegmentLoader
69
+ * @param {function } xhr for making a network request from the browser.
70
+ * @param {function } bandwidth for fetching the current bandwidth from the main segment loader.
70
71
*/
71
72
export default class ContentSteeringController extends videojs . EventTarget {
72
- // pass a segment loader reference for throughput rate and xhr
73
- constructor ( segmentLoader ) {
73
+ constructor ( xhr , bandwidth ) {
74
74
super ( ) ;
75
75
76
76
this . currentPathway = null ;
77
77
this . defaultPathway = null ;
78
78
this . queryBeforeStart = null ;
79
79
this . availablePathways_ = new Set ( ) ;
80
- // TODO: Implement exclusion.
81
80
this . excludedPathways_ = new Set ( ) ;
82
81
this . steeringManifest = new SteeringManifest ( ) ;
83
82
this . proxyServerUrl_ = null ;
84
83
this . manifestType_ = null ;
85
84
this . ttlTimeout_ = null ;
86
85
this . request_ = null ;
87
- this . mainSegmentLoader_ = segmentLoader ;
86
+ this . excludedSteeringManifestURLs = new Set ( ) ;
88
87
this . logger_ = logger ( 'Content Steering' ) ;
88
+ this . xhr_ = xhr ;
89
+ this . getBandwidth_ = bandwidth ;
89
90
}
90
91
91
92
/**
@@ -109,57 +110,93 @@ export default class ContentSteeringController extends videojs.EventTarget {
109
110
this . decodeDataUriManifest_ ( steeringUri . substring ( steeringUri . indexOf ( ',' ) + 1 ) ) ;
110
111
return ;
111
112
}
112
- this . steeringManifest . reloadUri = resolveUrl ( baseUrl , steeringUri ) ;
113
+ // With DASH queryBeforeStart, we want to use the steeringUri as soon as possible for the request.
114
+ this . steeringManifest . reloadUri = this . queryBeforeStart ? steeringUri : resolveUrl ( baseUrl , steeringUri ) ;
113
115
// pathwayId is HLS defaultServiceLocation is DASH
114
116
this . defaultPathway = steeringTag . pathwayId || steeringTag . defaultServiceLocation ;
115
117
// currently only DASH supports the following properties on <ContentSteering> tags.
116
- if ( this . manifestType_ === 'DASH' ) {
117
- this . queryBeforeStart = steeringTag . queryBeforeStart || false ;
118
- this . proxyServerUrl_ = steeringTag . proxyServerURL ;
119
- }
118
+ this . queryBeforeStart = steeringTag . queryBeforeStart || false ;
119
+ this . proxyServerUrl_ = steeringTag . proxyServerURL || null ;
120
120
121
121
// trigger a steering event if we have a pathway from the content steering tag.
122
122
// this tells VHS which segment pathway to start with.
123
- if ( this . defaultPathway ) {
123
+ // If queryBeforeStart is true we need to wait for the steering manifest response.
124
+ if ( this . defaultPathway && ! this . queryBeforeStart ) {
124
125
this . trigger ( 'content-steering' ) ;
125
126
}
127
+
128
+ if ( this . queryBeforeStart ) {
129
+ this . requestSteeringManifest ( this . steeringManifest . reloadUri ) ;
130
+ }
126
131
}
127
132
128
133
/**
129
134
* Requests the content steering manifest and parse the response. This should only be called after
130
135
* assignTagProperties was called with a content steering tag.
136
+ *
137
+ * @param {string } initialUri The optional uri to make the request with.
138
+ * If set, the request should be made with exactly what is passed in this variable.
139
+ * This scenario is specific to DASH when the queryBeforeStart parameter is true.
140
+ * This scenario should only happen once on initalization.
131
141
*/
132
- requestSteeringManifest ( ) {
133
- // add parameters to the steering uri
142
+ requestSteeringManifest ( initialUri ) {
134
143
const reloadUri = this . steeringManifest . reloadUri ;
144
+
145
+ if ( ! initialUri && ! reloadUri ) {
146
+ return ;
147
+ }
148
+
135
149
// We currently don't support passing MPD query parameters directly to the content steering URL as this requires
136
150
// ExtUrlQueryInfo tag support. See the DASH content steering spec section 8.1.
137
- const uri = this . proxyServerUrl_ ? this . setProxyServerUrl_ ( reloadUri ) : this . setSteeringParams_ ( reloadUri ) ;
138
151
139
- this . request_ = this . mainSegmentLoader_ . vhs_ . xhr ( {
152
+ // This request URI accounts for manifest URIs that have been excluded.
153
+ const uri = initialUri || this . getRequestURI ( reloadUri ) ;
154
+
155
+ // If there are no valid manifest URIs, we should stop content steering.
156
+ if ( ! uri ) {
157
+ this . logger_ ( 'No valid content steering manifest URIs. Stopping content steering.' ) ;
158
+ this . trigger ( 'error' ) ;
159
+ this . dispose ( ) ;
160
+ return ;
161
+ }
162
+
163
+ this . request_ = this . xhr_ ( {
140
164
uri
141
- } , ( error ) => {
142
- // TODO: HLS CASES THAT NEED ADDRESSED:
143
- // If the client receives HTTP 410 Gone in response to a manifest request,
144
- // it MUST NOT issue another request for that URI for the remainder of the
145
- // playback session. It MAY continue to use the most-recently obtained set
146
- // of Pathways.
147
- // If the client receives HTTP 429 Too Many Requests with a Retry-After
148
- // header in response to a manifest request, it SHOULD wait until the time
149
- // specified by the Retry-After header to reissue the request.
165
+ } , ( error , errorInfo ) => {
150
166
if ( error ) {
151
- // TODO: HLS RETRY CASE:
167
+ // If the client receives HTTP 410 Gone in response to a manifest request,
168
+ // it MUST NOT issue another request for that URI for the remainder of the
169
+ // playback session. It MAY continue to use the most-recently obtained set
170
+ // of Pathways.
171
+ if ( errorInfo . status === 410 ) {
172
+ this . logger_ ( `manifest request 410 ${ error } .` ) ;
173
+ this . logger_ ( `There will be no more content steering requests to ${ uri } this session.` ) ;
174
+
175
+ this . excludedSteeringManifestURLs . add ( uri ) ;
176
+ return ;
177
+ }
178
+ // If the client receives HTTP 429 Too Many Requests with a Retry-After
179
+ // header in response to a manifest request, it SHOULD wait until the time
180
+ // specified by the Retry-After header to reissue the request.
181
+ if ( errorInfo . status === 429 ) {
182
+ const retrySeconds = errorInfo . responseHeaders [ 'retry-after' ] ;
183
+
184
+ this . logger_ ( `manifest request 429 ${ error } .` ) ;
185
+ this . logger_ ( `content steering will retry in ${ retrySeconds } seconds.` ) ;
186
+ this . startTTLTimeout_ ( parseInt ( retrySeconds , 10 ) ) ;
187
+ return ;
188
+ }
152
189
// If the Steering Manifest cannot be loaded and parsed correctly, the
153
190
// client SHOULD continue to use the previous values and attempt to reload
154
191
// it after waiting for the previously-specified TTL (or 5 minutes if
155
192
// none).
156
193
this . logger_ ( `manifest failed to load ${ error } .` ) ;
157
- // TODO: we may want to expose the error object here.
158
- this . trigger ( 'error' ) ;
194
+ this . startTTLTimeout_ ( ) ;
159
195
return ;
160
196
}
161
197
const steeringManifestJson = JSON . parse ( this . request_ . responseText ) ;
162
198
199
+ this . startTTLTimeout_ ( ) ;
163
200
this . assignSteeringProperties_ ( steeringManifestJson ) ;
164
201
} ) ;
165
202
}
@@ -200,18 +237,18 @@ export default class ContentSteeringController extends videojs.EventTarget {
200
237
setSteeringParams_ ( url ) {
201
238
const urlObject = new window . URL ( url ) ;
202
239
const path = this . getPathway ( ) ;
240
+ const networkThroughput = this . getBandwidth_ ( ) ;
203
241
204
242
if ( path ) {
205
243
const pathwayKey = `_${ this . manifestType_ } _pathway` ;
206
244
207
245
urlObject . searchParams . set ( pathwayKey , path ) ;
208
246
}
209
247
210
- if ( this . mainSegmentLoader_ . throughput . rate ) {
248
+ if ( networkThroughput ) {
211
249
const throughputKey = `_${ this . manifestType_ } _throughput` ;
212
- const rateInteger = Math . round ( this . mainSegmentLoader_ . throughput . rate ) ;
213
250
214
- urlObject . searchParams . set ( throughputKey , rateInteger ) ;
251
+ urlObject . searchParams . set ( throughputKey , networkThroughput ) ;
215
252
}
216
253
return urlObject . toString ( ) ;
217
254
}
@@ -234,44 +271,91 @@ export default class ContentSteeringController extends videojs.EventTarget {
234
271
this . steeringManifest . priority = steeringJson [ 'PATHWAY-PRIORITY' ] || steeringJson [ 'SERVICE-LOCATION-PRIORITY' ] ;
235
272
// TODO: HLS handle PATHWAY-CLONES. See section 7.2 https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/
236
273
237
- // TODO: fully implement priority logic.
238
274
// 1. apply first pathway from the array.
239
- // 2. if first first pathway doesn't exist in manifest, try next pathway.
275
+ // 2. if first pathway doesn't exist in manifest, try next pathway.
240
276
// a. if all pathways are exhausted, ignore the steering manifest priority.
241
277
// 3. if segments fail from an established pathway, try all variants/renditions, then exclude the failed pathway.
242
278
// a. exclude a pathway for a minimum of the last TTL duration. Meaning, from the next steering response,
243
279
// the excluded pathway will be ignored.
244
- const chooseNextPathway = ( pathways ) => {
245
- for ( const path of pathways ) {
280
+ // See excludePathway usage in excludePlaylist().
281
+
282
+ // If there are no available pathways, we need to stop content steering.
283
+ if ( ! this . availablePathways_ . size ) {
284
+ this . logger_ ( 'There are no available pathways for content steering. Ending content steering.' ) ;
285
+ this . trigger ( 'error' ) ;
286
+ this . dispose ( ) ;
287
+ }
288
+
289
+ const chooseNextPathway = ( pathwaysByPriority ) => {
290
+ for ( const path of pathwaysByPriority ) {
246
291
if ( this . availablePathways_ . has ( path ) ) {
247
292
return path ;
248
293
}
249
294
}
295
+
296
+ // If no pathway matches, ignore the manifest and choose the first available.
297
+ return [ ...this . availablePathways_ ] [ 0 ] ;
250
298
} ;
299
+
251
300
const nextPathway = chooseNextPathway ( this . steeringManifest . priority ) ;
252
301
253
302
if ( this . currentPathway !== nextPathway ) {
254
303
this . currentPathway = nextPathway ;
255
304
this . trigger ( 'content-steering' ) ;
256
305
}
257
- this . startTTLTimeout_ ( ) ;
258
306
}
259
307
260
308
/**
261
309
* Returns the pathway to use for steering decisions
262
310
*
263
- * @return returns the current pathway or the default
311
+ * @return { string } returns the current pathway or the default
264
312
*/
265
313
getPathway ( ) {
266
314
return this . currentPathway || this . defaultPathway ;
267
315
}
268
316
317
+ /**
318
+ * Chooses the manifest request URI based on proxy URIs and server URLs.
319
+ * Also accounts for exclusion on certain manifest URIs.
320
+ *
321
+ * @param {string } reloadUri the base uri before parameters
322
+ *
323
+ * @return {string } the final URI for the request to the manifest server.
324
+ */
325
+ getRequestURI ( reloadUri ) {
326
+ if ( ! reloadUri ) {
327
+ return null ;
328
+ }
329
+
330
+ const isExcluded = ( uri ) => this . excludedSteeringManifestURLs . has ( uri ) ;
331
+
332
+ if ( this . proxyServerUrl_ ) {
333
+ const proxyURI = this . setProxyServerUrl_ ( reloadUri ) ;
334
+
335
+ if ( ! isExcluded ( proxyURI ) ) {
336
+ return proxyURI ;
337
+ }
338
+ }
339
+
340
+ const steeringURI = this . setSteeringParams_ ( reloadUri ) ;
341
+
342
+ if ( ! isExcluded ( steeringURI ) ) {
343
+ return steeringURI ;
344
+ }
345
+
346
+ // Return nothing if all valid manifest URIs are excluded.
347
+ return null ;
348
+ }
349
+
269
350
/**
270
351
* Start the timeout for re-requesting the steering manifest at the TTL interval.
352
+ *
353
+ * @param {number } ttl time in seconds of the timeout. Defaults to the
354
+ * ttl interval in the steering manifest
271
355
*/
272
- startTTLTimeout_ ( ) {
356
+ startTTLTimeout_ ( ttl = this . steeringManifest . ttl ) {
273
357
// 300 (5 minutes) is the default value.
274
- const ttlMS = this . steeringManifest . ttl * 1000 ;
358
+ const ttlMS = ttl * 1000 ;
275
359
276
360
this . ttlTimeout_ = window . setTimeout ( ( ) => {
277
361
this . requestSteeringManifest ( ) ;
@@ -300,6 +384,8 @@ export default class ContentSteeringController extends videojs.EventTarget {
300
384
* aborts steering requests clears the ttl timeout and resets all properties.
301
385
*/
302
386
dispose ( ) {
387
+ this . off ( 'content-steering' ) ;
388
+ this . off ( 'error' ) ;
303
389
this . abort ( ) ;
304
390
this . clearTTLTimeout_ ( ) ;
305
391
this . currentPathway = null ;
@@ -309,6 +395,7 @@ export default class ContentSteeringController extends videojs.EventTarget {
309
395
this . manifestType_ = null ;
310
396
this . ttlTimeout_ = null ;
311
397
this . request_ = null ;
398
+ this . excludedSteeringManifestURLs = new Set ( ) ;
312
399
this . availablePathways_ = new Set ( ) ;
313
400
this . excludedPathways_ = new Set ( ) ;
314
401
this . steeringManifest = new SteeringManifest ( ) ;
@@ -320,6 +407,19 @@ export default class ContentSteeringController extends videojs.EventTarget {
320
407
* @param {string } pathway the pathway string to add
321
408
*/
322
409
addAvailablePathway ( pathway ) {
323
- this . availablePathways_ . add ( pathway ) ;
410
+ if ( pathway ) {
411
+ this . availablePathways_ . add ( pathway ) ;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * clears all pathways from the available pathways set
417
+ */
418
+ clearAvailablePathways ( ) {
419
+ this . availablePathways_ . clear ( ) ;
420
+ }
421
+
422
+ excludePathway ( pathway ) {
423
+ return this . availablePathways_ . delete ( pathway ) ;
324
424
}
325
425
}
0 commit comments