@@ -2,7 +2,7 @@ import BufferOperationQueue from './buffer-operation-queue';
22import { createDoNothingErrorAction } from './error-controller' ;
33import { ErrorDetails , ErrorTypes } from '../errors' ;
44import { Events } from '../events' ;
5- import { ElementaryStreamTypes , isMediaFragment } from '../loader/fragment' ;
5+ import { ElementaryStreamTypes } from '../loader/fragment' ;
66import { DEFAULT_TARGET_DURATION } from '../loader/level-details' ;
77import { PlaylistLevelType } from '../types/loader' ;
88import { BufferHelper } from '../utils/buffer-helper' ;
@@ -70,6 +70,8 @@ const VIDEO_CODEC_PROFILE_REPLACE =
7070
7171const TRACK_REMOVED_ERROR_NAME = 'HlsJsTrackRemovedError' ;
7272
73+ const LOOP_FLUSH_SAFETY_MARGIN = 0.25 ;
74+
7375class HlsJsTrackRemovedError extends Error {
7476 constructor ( message ) {
7577 super ( message ) ;
@@ -1131,36 +1133,39 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
11311133 return ;
11321134 }
11331135 const { backBufferLength, frontBufferFlushThreshold } = config ;
1134- this . trimBuffers ( frontBufferFlushThreshold , backBufferLength ) ;
1136+ this . trimBuffers (
1137+ frontBufferFlushThreshold ,
1138+ backBufferLength ,
1139+ data . frag ,
1140+ data . previousFrag ,
1141+ ) ;
11351142
11361143 // Only clear append errors on successful encounter of buffered media. Init segments may complete without error for unsupported media.
1137- if ( isMediaFragment ( data . frag ) ) {
1138- const elementaryStreams = data . frag . elementaryStreams ;
1139- const { appendErrors } = this ;
1140- const appendErrorType = this . appendError ?. sourceBufferName ;
1144+ const elementaryStreams = data . frag . elementaryStreams ;
1145+ const { appendErrors } = this ;
1146+ const appendErrorType = this . appendError ?. sourceBufferName ;
11411147
1142- Object . keys ( elementaryStreams ) . forEach ( ( type ) => {
1143- if ( ! elementaryStreams [ type ] ) {
1144- return ;
1145- }
1146- appendErrors [ type ] = 0 ;
1147- if ( type === appendErrorType ) {
1148+ Object . keys ( elementaryStreams ) . forEach ( ( type ) => {
1149+ if ( ! elementaryStreams [ type ] ) {
1150+ return ;
1151+ }
1152+ appendErrors [ type ] = 0 ;
1153+ if ( type === appendErrorType ) {
1154+ this . appendError = undefined ;
1155+ }
1156+ if ( type === 'audio' || type === 'video' ) {
1157+ appendErrors . audiovideo = 0 ;
1158+ if ( appendErrorType === 'audiovideo' ) {
11481159 this . appendError = undefined ;
11491160 }
1150- if ( type === 'audio' || type === 'video' ) {
1151- appendErrors . audiovideo = 0 ;
1152- if ( appendErrorType === 'audiovideo' ) {
1153- this . appendError = undefined ;
1154- }
1155- } else {
1156- appendErrors . audio = 0 ;
1157- appendErrors . video = 0 ;
1158- if ( appendErrorType !== 'audiovideo' ) {
1159- this . appendError = undefined ;
1160- }
1161+ } else {
1162+ appendErrors . audio = 0 ;
1163+ appendErrors . video = 0 ;
1164+ if ( appendErrorType !== 'audiovideo' ) {
1165+ this . appendError = undefined ;
11611166 }
1162- } ) ;
1163- }
1167+ }
1168+ } ) ;
11641169 }
11651170
11661171 public get bufferedToEnd ( ) : boolean {
@@ -1303,6 +1308,8 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
13031308 private trimBuffers (
13041309 frontBufferFlushThreshold : number ,
13051310 backBufferLength : number ,
1311+ frag ?: MediaFragment ,
1312+ previousFrag ?: MediaFragment | null ,
13061313 ) {
13071314 const { hls, details, media } = this ;
13081315 if ( ! media || details === null ) {
@@ -1323,12 +1330,30 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
13231330 ? config . liveBackBufferLength
13241331 : backBufferLength ;
13251332
1333+ let targetBackBufferPosition = - Infinity ;
13261334 if ( Number . isFinite ( backBufferLength ) && backBufferLength >= 0 ) {
13271335 const maxBackBufferLength = Math . max ( backBufferLength , targetDuration ) ;
1328- const targetBackBufferPosition =
1336+ targetBackBufferPosition =
13291337 Math . floor ( currentTime / targetDuration ) * targetDuration -
13301338 maxBackBufferLength ;
1339+ }
1340+
1341+ // For looped media with a quality upgrade, extend the flush position
1342+ // to remove lower-quality segments from the back buffer.
1343+ if ( frag ) {
1344+ const loopFlushEnd = this . getLoopBackBufferFlushEnd (
1345+ frag ,
1346+ previousFrag ?? null ,
1347+ ) ;
1348+ if ( loopFlushEnd > 0 ) {
1349+ targetBackBufferPosition = Math . max (
1350+ targetBackBufferPosition ,
1351+ loopFlushEnd ,
1352+ ) ;
1353+ }
1354+ }
13311355
1356+ if ( targetBackBufferPosition > 0 ) {
13321357 this . flushBackBuffer (
13331358 currentTime ,
13341359 targetDuration ,
@@ -1358,6 +1383,57 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
13581383 }
13591384 }
13601385
1386+ /**
1387+ * For looped media, determine the back buffer flush position to remove
1388+ * lower-quality segments on a quality upgrade. Returns 0 if no flush is needed.
1389+ */
1390+ private getLoopBackBufferFlushEnd (
1391+ frag : MediaFragment ,
1392+ previousFrag : MediaFragment | null ,
1393+ ) : number {
1394+ const { media } = this ;
1395+ if (
1396+ this . hls ?. config . loopBackBufferFlush === false ||
1397+ ! media ?. loop ||
1398+ ! previousFrag ||
1399+ frag . level <= previousFrag . level
1400+ ) {
1401+ return 0 ;
1402+ }
1403+
1404+ const { video, audiovideo } = frag . elementaryStreams ;
1405+ if ( video ?. partial || audiovideo ?. partial ) {
1406+ return 0 ;
1407+ }
1408+
1409+ const flushEnd =
1410+ this . getEarliestElementaryStreamStart ( frag ) - LOOP_FLUSH_SAFETY_MARGIN ;
1411+ if ( flushEnd <= 0 ) {
1412+ return 0 ;
1413+ }
1414+
1415+ this . log (
1416+ `Flushing lower quality back buffer for loop: level ${ frag . level } , range [0-${ flushEnd . toFixed ( 3 ) } ]` ,
1417+ ) ;
1418+ return flushEnd ;
1419+ }
1420+
1421+ private getEarliestElementaryStreamStart ( frag : MediaFragment ) : number {
1422+ const { audio, video, audiovideo } = frag . elementaryStreams ;
1423+ let earliest = frag . start ;
1424+ if ( audiovideo ) {
1425+ earliest = Math . min ( earliest , audiovideo . startDTS ) ;
1426+ } else {
1427+ if ( audio ) {
1428+ earliest = Math . min ( earliest , audio . startDTS ) ;
1429+ }
1430+ if ( video ) {
1431+ earliest = Math . min ( earliest , video . startDTS ) ;
1432+ }
1433+ }
1434+ return earliest ;
1435+ }
1436+
13611437 private flushBackBuffer (
13621438 currentTime : number ,
13631439 targetDuration : number ,
@@ -1381,7 +1457,12 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
13811457 this . hls . trigger ( Events . LIVE_BACK_BUFFER_REACHED , {
13821458 bufferEnd : targetBackBufferPosition ,
13831459 } ) ;
1384- } else if ( track ?. ended ) {
1460+ } else if (
1461+ track ?. ended &&
1462+ ! (
1463+ this . media ?. loop && this . hls ?. config . loopBackBufferFlush !== false
1464+ )
1465+ ) {
13851466 this . log (
13861467 `Cannot flush ${ type } back buffer while SourceBuffer is in ended state` ,
13871468 ) ;
0 commit comments