@@ -7,13 +7,16 @@ import {
7
7
shouldRetry ,
8
8
} from '../utils/error-helper' ;
9
9
import { findFragmentByPTS } from './fragment-finders' ;
10
- import { HdcpLevel , HdcpLevels } from '../types/level' ;
10
+ import { HdcpLevel , HdcpLevels , type Level } from '../types/level' ;
11
11
import { logger } from '../utils/logger' ;
12
12
import type Hls from '../hls' ;
13
13
import type { RetryConfig } from '../config' ;
14
14
import type { NetworkComponentAPI } from '../types/component-api' ;
15
15
import type { ErrorData } from '../types/events' ;
16
16
import type { Fragment } from '../loader/fragment' ;
17
+ import type { LevelDetails } from '../hls' ;
18
+
19
+ const RENDITION_PENALTY_DURATION_MS = 300000 ;
17
20
18
21
export const enum NetworkErrorAction {
19
22
DoNothing = 0 ,
@@ -41,10 +44,18 @@ export type IErrorAction = {
41
44
resolved ?: boolean ;
42
45
} ;
43
46
47
+ type PenalizedRendition = {
48
+ lastErrorPerfMs : number ;
49
+ errors : ErrorData [ ] ;
50
+ details ?: LevelDetails ;
51
+ } ;
52
+
53
+ type PenalizedRenditions = { [ key : number ] : PenalizedRendition } ;
54
+
44
55
export default class ErrorController implements NetworkComponentAPI {
45
56
private readonly hls : Hls ;
46
57
private playlistError : number = 0 ;
47
- private failoverError ?: ErrorData ;
58
+ private penalizedRenditions : PenalizedRenditions = { } ;
48
59
private log : ( msg : any ) => void ;
49
60
private warn : ( msg : any ) => void ;
50
61
private error : ( msg : any ) => void ;
@@ -58,12 +69,19 @@ export default class ErrorController implements NetworkComponentAPI {
58
69
}
59
70
60
71
private registerListeners ( ) {
61
- this . hls . on ( Events . ERROR , this . onError , this ) ;
72
+ const hls = this . hls ;
73
+ hls . on ( Events . ERROR , this . onError , this ) ;
74
+ hls . on ( Events . MANIFEST_LOADING , this . onManifestLoading , this ) ;
62
75
}
63
76
64
77
private unregisterListeners ( ) {
65
- this . hls . off ( Events . ERROR , this . onError , this ) ;
66
- this . hls . off ( Events . ERROR , this . onErrorOut , this ) ;
78
+ const hls = this . hls ;
79
+ if ( ! hls ) {
80
+ return ;
81
+ }
82
+ hls . off ( Events . ERROR , this . onError , this ) ;
83
+ hls . off ( Events . ERROR , this . onErrorOut , this ) ;
84
+ hls . off ( Events . MANIFEST_LOADING , this . onManifestLoading , this ) ;
67
85
}
68
86
69
87
destroy ( ) {
@@ -84,6 +102,11 @@ export default class ErrorController implements NetworkComponentAPI {
84
102
: this . hls . loadLevel ;
85
103
}
86
104
105
+ private onManifestLoading ( ) {
106
+ this . playlistError = 0 ;
107
+ this . penalizedRenditions = { } ;
108
+ }
109
+
87
110
private onError ( event : Events . ERROR , data : ErrorData ) {
88
111
if ( data . fatal ) {
89
112
return ;
@@ -446,38 +469,95 @@ export default class ErrorController implements NetworkComponentAPI {
446
469
}
447
470
448
471
private redundantFailover ( data : ErrorData ) : boolean {
449
- const { hls, failoverError } = this ;
472
+ const { hls, penalizedRenditions } = this ;
450
473
const levelIndex : number =
451
474
data . parent === PlaylistLevelType . MAIN
452
475
? ( data . level as number )
453
476
: hls . loadLevel ;
454
477
const level = hls . levels [ levelIndex ] ;
455
478
const redundantLevels = level . url . length ;
456
- const newUrlId = ( level . urlId + 1 ) % redundantLevels ;
479
+ this . penalizeRendition ( level , data ) ;
480
+ for ( let i = 1 ; i < redundantLevels ; i ++ ) {
481
+ const newUrlId = ( level . urlId + i ) % redundantLevels ;
482
+ const penalizedRendition = penalizedRenditions [ newUrlId ] ;
483
+ // Check if rendition is penalized and skip if it is a bad fit for failover
484
+ if (
485
+ ! penalizedRendition ||
486
+ checkExpired ( penalizedRendition , data , penalizedRenditions [ level . urlId ] )
487
+ ) {
488
+ // delete penalizedRenditions[newUrlId];
489
+ // Update the url id of all levels so that we stay on the same set of variants when level switching
490
+ this . warn (
491
+ `Switching to Redundant Stream ${ newUrlId + 1 } /${ redundantLevels } : "${
492
+ level . url [ newUrlId ]
493
+ } " after ${ data . details } `
494
+ ) ;
495
+ this . playlistError = 0 ;
496
+ hls . levels . forEach ( ( lv ) => {
497
+ lv . urlId = newUrlId ;
498
+ } ) ;
499
+ hls . nextLoadLevel = levelIndex ;
500
+ return true ;
501
+ }
502
+ }
503
+ return false ;
504
+ }
505
+
506
+ private penalizeRendition ( level : Level , data : ErrorData ) {
507
+ const { penalizedRenditions } = this ;
508
+ const penalizedRendition = penalizedRenditions [ level . urlId ] || {
509
+ lastErrorPerfMs : 0 ,
510
+ errors : [ ] ,
511
+ details : undefined ,
512
+ } ;
513
+ penalizedRendition . lastErrorPerfMs = performance . now ( ) ;
514
+ penalizedRendition . errors . push ( data ) ;
515
+ penalizedRendition . details = level . details ;
516
+ penalizedRenditions [ level . urlId ] = penalizedRendition ;
517
+ }
518
+ }
519
+
520
+ function checkExpired (
521
+ penalizedRendition : PenalizedRendition ,
522
+ data : ErrorData ,
523
+ currentPenaltyState : PenalizedRendition | undefined
524
+ ) : boolean {
525
+ // Expire penalty for switching back to rendition after RENDITION_PENALTY_DURATION_MS
526
+ if (
527
+ performance . now ( ) - penalizedRendition . lastErrorPerfMs >
528
+ RENDITION_PENALTY_DURATION_MS
529
+ ) {
530
+ return true ;
531
+ }
532
+ // Expire penalty on GAP tag error if rendition has no GAP at position (does not cover media tracks)
533
+ const lastErrorDetails = penalizedRendition . details ;
534
+ if ( data . details === ErrorDetails . FRAG_GAP && lastErrorDetails && data . frag ) {
535
+ const position = data . frag . start ;
536
+ const candidateFrag = findFragmentByPTS (
537
+ null ,
538
+ lastErrorDetails . fragments ,
539
+ position
540
+ ) ;
541
+ if ( candidateFrag && ! candidateFrag . gap ) {
542
+ return true ;
543
+ }
544
+ }
545
+ // Expire penalty if there are more errors in currentLevel than in penalizedRendition
546
+ if (
547
+ currentPenaltyState &&
548
+ penalizedRendition . errors . length < currentPenaltyState . errors . length
549
+ ) {
550
+ const lastCandidateError =
551
+ penalizedRendition . errors [ penalizedRendition . errors . length - 1 ] ;
457
552
if (
458
- redundantLevels > 1 &&
459
- // FIXME: Don't loop back to first redundant renditions unless the last gap is more than 60 seconds ago
460
- // TODO: We throw out information about the other redundant renditions but should keep track of gap ranges to avoid looping back to the same problem
461
- ( newUrlId !== 0 ||
462
- ! failoverError ||
463
- ( failoverError . frag &&
464
- data . frag &&
465
- Math . abs ( data . frag . start - failoverError . frag . start ) > 60 ) )
553
+ lastErrorDetails &&
554
+ lastCandidateError . frag &&
555
+ data . frag &&
556
+ Math . abs ( lastCandidateError . frag . start - data . frag . start ) >
557
+ lastErrorDetails . targetduration * 3
466
558
) {
467
- this . failoverError = data ;
468
- // Update the url id of all levels so that we stay on the same set of variants when level switching
469
- this . log (
470
- `Switching to Redundant Stream ${ newUrlId + 1 } /${ redundantLevels } : "${
471
- level . url [ newUrlId ]
472
- } "`
473
- ) ;
474
- this . playlistError = 0 ;
475
- hls . levels . forEach ( ( lv ) => {
476
- lv . urlId = newUrlId ;
477
- } ) ;
478
- hls . nextLoadLevel = levelIndex ;
479
559
return true ;
480
560
}
481
- return false ;
482
561
}
562
+ return false ;
483
563
}
0 commit comments