@@ -118,6 +118,12 @@ export default class BufferController extends Logger implements ComponentAPI {
118118 audiovideo : 0 ,
119119 } ;
120120 private appendError ?: ErrorData ;
121+ // Tracks whether a QuotaExceededError back-buffer eviction is in progress
122+ // for a given SourceBuffer type. While true, subsequent QuotaExceededErrors
123+ // on the same type piggyback on the pending eviction instead of triggering
124+ // their own or emitting BUFFER_FULL_ERROR.
125+ private _quotaEvictionPending : Partial < Record < SourceBufferName , boolean > > =
126+ { } ;
121127 // Record of required or created buffers by type. SourceBuffer is stored in Track.buffer once created.
122128 private tracks : SourceBufferTrackSet = { } ;
123129 // Array of SourceBuffer type and SourceBuffer (or null). One entry per TrackSet in this.tracks.
@@ -853,6 +859,7 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
853859 }
854860
855861 const fragStart = ( part || frag ) . start ;
862+ let quotaEvictionAttempted = false ;
856863 const operation : BufferOperation = {
857864 label : `append-${ type } ` ,
858865 execute : ( ) => {
@@ -900,6 +907,46 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
900907 } ,
901908 onError : ( error : Error ) => {
902909 this . clearBufferAppendTimeoutId ( this . tracks [ type ] ) ;
910+
911+ const isQuotaError =
912+ ( error as DOMException ) . code === DOMException . QUOTA_EXCEEDED_ERR ||
913+ error . name == 'QuotaExceededError' ||
914+ `quota` in error ;
915+
916+ if ( isQuotaError ) {
917+ // An eviction is already in flight for this type — piggyback on it
918+ // by queuing a retry after the pending remove, no new eviction needed.
919+ if ( this . _quotaEvictionPending [ type ] ) {
920+ this . log (
921+ `QuotaExceededError on "${ type } " sn: ${ sn } — eviction already pending, queuing retry` ,
922+ ) ;
923+ this . insertNext ( [ operation ] , type ) ;
924+ return ;
925+ }
926+
927+ // First QuotaExceededError for this type: evict minimum back buffer
928+ if ( ! quotaEvictionAttempted ) {
929+ const evictEnd = this . getBackBufferEvictionTarget (
930+ type ,
931+ data . byteLength ,
932+ frag . type ,
933+ ) ;
934+ if ( evictEnd > 0 ) {
935+ quotaEvictionAttempted = true ;
936+ this . _quotaEvictionPending [ type ] = true ;
937+ this . log (
938+ `QuotaExceededError on "${ type } " append sn: ${ sn } — evicting back buffer to ${ evictEnd . toFixed ( 3 ) } s and retrying` ,
939+ ) ;
940+ const removeOp = this . getQuotaEvictionFlushOp ( type , 0 , evictEnd ) ;
941+ this . insertNext ( [ removeOp , operation ] , type ) ;
942+ return ;
943+ }
944+ this . warn (
945+ `QuotaExceededError on "${ type } " sn: ${ sn } — no back buffer available to evict` ,
946+ ) ;
947+ }
948+ }
949+
903950 // in case any error occured while appending, put back segment in segments table
904951 const event : ErrorData = {
905952 type : ErrorTypes . MEDIA_ERROR ,
@@ -914,13 +961,9 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
914961 fatal : false ,
915962 } ;
916963 const mediaError = this . media ?. error ;
917- if (
918- ( error as DOMException ) . code === DOMException . QUOTA_EXCEEDED_ERR ||
919- error . name == 'QuotaExceededError' ||
920- `quota` in error
921- ) {
964+ if ( isQuotaError ) {
922965 // QuotaExceededError: http://www.w3.org/TR/html5/infrastructure.html#quotaexceedederror
923- // let's stop appending any segments, and report BUFFER_FULL_ERROR error
966+ // Eviction was already attempted or not possible — report BUFFER_FULL_ERROR
924967 event . details = ErrorDetails . BUFFER_FULL_ERROR ;
925968 } else if (
926969 ( error as DOMException ) . code === DOMException . INVALID_STATE_ERR &&
@@ -971,6 +1014,33 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
9711014 this . append ( operation , type , this . isPending ( this . tracks [ type ] ) ) ;
9721015 }
9731016
1017+ // Like getFlushOp but clears _quotaEvictionPending on complete/error
1018+ private getQuotaEvictionFlushOp (
1019+ type : SourceBufferName ,
1020+ start : number ,
1021+ end : number ,
1022+ ) : BufferOperation {
1023+ this . log ( `queuing quota-eviction "${ type } " remove ${ start } -${ end } ` ) ;
1024+ return {
1025+ label : 'remove' ,
1026+ execute : ( ) => {
1027+ this . removeExecutor ( type , start , end ) ;
1028+ } ,
1029+ onStart : ( ) => { } ,
1030+ onComplete : ( ) => {
1031+ this . _quotaEvictionPending [ type ] = false ;
1032+ this . hls . trigger ( Events . BUFFER_FLUSHED , { type } ) ;
1033+ } ,
1034+ onError : ( error : Error ) => {
1035+ this . _quotaEvictionPending [ type ] = false ;
1036+ this . warn (
1037+ `Failed to remove ${ start } -${ end } from "${ type } " SourceBuffer (quota eviction)` ,
1038+ error ,
1039+ ) ;
1040+ } ,
1041+ } ;
1042+ }
1043+
9741044 private getFlushOp (
9751045 type : SourceBufferName ,
9761046 start : number ,
@@ -1198,6 +1268,26 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
11981268 }
11991269 }
12001270
1271+ // Returns the end position to evict back buffer to on QuotaExceededError,
1272+ // or 0 if there is nothing to evict. Delegates to the fragment tracker which
1273+ // walks buffered fragments oldest-first using actual byte sizes to find the
1274+ // minimum eviction needed to fit the new segment.
1275+ private getBackBufferEvictionTarget (
1276+ type : SourceBufferName ,
1277+ segmentBytes : number ,
1278+ playlistType : PlaylistLevelType ,
1279+ ) : number {
1280+ const { media } = this ;
1281+ if ( ! media ) {
1282+ return 0 ;
1283+ }
1284+ return this . fragmentTracker . getBackBufferEvictionEnd (
1285+ media . currentTime ,
1286+ playlistType ,
1287+ segmentBytes ,
1288+ ) ;
1289+ }
1290+
12011291 private resetAppendErrors ( ) {
12021292 this . appendErrors = {
12031293 audio : 0 ,
@@ -1940,6 +2030,12 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
19402030 }
19412031 }
19422032
2033+ private insertNext ( operations : BufferOperation [ ] , type : SourceBufferName ) {
2034+ if ( this . operationQueue ) {
2035+ this . operationQueue . insertNext ( operations , type ) ;
2036+ }
2037+ }
2038+
19432039 private appendBlocker ( type : SourceBufferName ) : Promise < void > | undefined {
19442040 if ( this . operationQueue ) {
19452041 return this . operationQueue . appendBlocker ( type ) ;
0 commit comments