@@ -189,7 +189,7 @@ export default class MP4Remuxer implements Remuxer {
189189 } ;
190190 if ( computePTSDTS ) {
191191 // remember first PTS of this demuxing context. for audio, PTS = DTS
192- initPTS = initDTS = audioSamples [ 0 ] . pts - audioTrack . inputTimeScale * timeOffset ;
192+ initPTS = initDTS = audioSamples [ 0 ] . pts - Math . round ( audioTrack . inputTimeScale * timeOffset ) ;
193193 }
194194 }
195195
@@ -208,8 +208,9 @@ export default class MP4Remuxer implements Remuxer {
208208 }
209209 } ;
210210 if ( computePTSDTS ) {
211- initPTS = Math . min ( initPTS , videoSamples [ 0 ] . pts - inputTimeScale * timeOffset ) ;
212- initDTS = Math . min ( initDTS , videoSamples [ 0 ] . dts - inputTimeScale * timeOffset ) ;
211+ const startPTS = Math . round ( inputTimeScale * timeOffset ) ;
212+ initPTS = Math . min ( initPTS , videoSamples [ 0 ] . pts - startPTS ) ;
213+ initDTS = Math . min ( initDTS , videoSamples [ 0 ] . dts - startPTS ) ;
213214 }
214215 }
215216
@@ -235,6 +236,8 @@ export default class MP4Remuxer implements Remuxer {
235236 const initPTS : number = this . _initPTS ;
236237 let nextAvcDts = this . nextAvcDts ;
237238 let offset = 8 ;
239+ let minPTS : number = Number . MAX_SAFE_INTEGER ;
240+ let maxPTS : number = - Number . MAX_SAFE_INTEGER ;
238241 let mp4SampleDuration ! : number ;
239242
240243 // Safari does not like overlapping DTS on consecutive fragments. let's use nextAvcDts to overcome this if fragments are consecutive
@@ -262,51 +265,54 @@ export default class MP4Remuxer implements Remuxer {
262265 inputSamples . forEach ( function ( sample ) {
263266 sample . pts = PTSNormalize ( sample . pts - initPTS , nextAvcDts ) ;
264267 sample . dts = PTSNormalize ( sample . dts - initPTS , nextAvcDts ) ;
268+ minPTS = Math . min ( sample . pts , minPTS ) ;
269+ maxPTS = Math . max ( sample . pts , maxPTS ) ;
265270 } ) ;
266271
267- // handle broken streams with PTS < DTS, tolerance up 200ms (18000 in 90kHz timescale)
268- const PTSDTSshift = inputSamples . reduce ( ( prev , curr ) => Math . max ( Math . min ( prev , curr . pts - curr . dts ) , - 18000 ) , 0 ) ;
269- if ( PTSDTSshift < 0 ) {
270- logger . log ( `[mp4-remuxer]: PTS < DTS detected in video samples, shifting DTS by ${ Math . round ( PTSDTSshift / 90 ) } ms to overcome this issue` ) ;
271- for ( let i = 0 ; i < inputSamples . length ; i ++ ) {
272- inputSamples [ i ] . dts += PTSDTSshift ;
273- }
274- }
275-
276- const firstSample = inputSamples [ 0 ] ;
277- let firstDTS = Math . max ( firstSample . dts , 0 ) ;
278- let firstPTS = Math . max ( firstSample . pts , 0 ) ;
272+ // Get first/last DTS
273+ let firstDTS = inputSamples [ 0 ] . dts ;
274+ const lastDTS = inputSamples [ inputSamples . length - 1 ] . dts ;
279275
280276 // Check timestamp continuity across consecutive fragments, and modify timing in order to remove gaps or overlaps.
281- const millisecondDelta = Math . round ( ( firstDTS - nextAvcDts ) / 90 ) ;
282277 if ( contiguous ) {
283- if ( millisecondDelta ) {
284- if ( millisecondDelta > 1 ) {
285- logger . log ( `[mp4-remuxer]: AVC:${ millisecondDelta } ms hole between fragments detected,filling it` ) ;
286- } else if ( millisecondDelta < - 1 ) {
287- logger . log ( `[mp4-remuxer]: AVC:${ ( - millisecondDelta ) } ms overlapping between fragments detected` ) ;
278+ const delta = firstDTS - nextAvcDts ;
279+ const foundHole = delta > 2 ;
280+ const foundOverlap = delta < - 1 ;
281+ if ( foundHole || foundOverlap ) {
282+ const millisecondDelta = Math . round ( delta / 90 ) ;
283+ if ( foundHole ) {
284+ logger . warn ( `AVC: ${ millisecondDelta } ms (${ delta } dts) hole between fragments detected, filling it` ) ;
285+ } else {
286+ logger . warn ( `AVC: ${ - millisecondDelta } ms (${ delta } dts) overlapping between fragments detected` ) ;
288287 }
289-
290- // remove hole/gap : set DTS to next expected DTS
291- firstSample . dts = firstDTS = nextAvcDts ;
292- firstSample . pts = firstPTS = Math . max ( firstSample . pts - millisecondDelta , nextAvcDts ) ;
293- // offset PTS as well, ensure that PTS is smaller or equal than new DTS
294- logger . log ( `[mp4-remuxer]: Video/PTS/DTS adjusted: ${ Math . round ( firstSample . pts / 90 ) } /${ Math . round ( firstDTS / 90 ) } , delta:${ millisecondDelta } ms` ) ;
288+ firstDTS = nextAvcDts ;
289+ minPTS -= delta ;
290+ inputSamples [ 0 ] . dts = firstDTS ;
291+ inputSamples [ 0 ] . pts = minPTS ;
292+ logger . log ( `Video: PTS/DTS adjusted: ${ Math . round ( minPTS / 90 ) } /${ Math . round ( firstDTS / 90 ) } , delta: ${ millisecondDelta } ms` ) ;
295293 }
296294 }
297295
298- // compute lastPTS/lastDTS
299- const lastSample = inputSamples [ inputSamples . length - 1 ] ;
300- const lastDTS = Math . max ( lastSample . dts , 0 ) ;
301- const lastPTS = Math . max ( lastSample . pts , 0 , lastDTS ) ;
302-
303296 // on Safari let's signal the same sample duration for all samples
304297 // sample duration (as expected by trun MP4 boxes), should be the delta between sample DTS
305298 // set this constant duration as being the avg delta between consecutive DTS.
306299 if ( isSafari ) {
307300 mp4SampleDuration = Math . round ( ( lastDTS - firstDTS ) / ( inputSamples . length - 1 ) ) ;
308301 }
309302
303+ // handle broken streams with PTS < DTS, tolerance up 200ms (18000 in 90kHz timescale)
304+ const PTSDTSshift = inputSamples . reduce ( ( prev , curr ) => Math . max ( Math . min ( prev , curr . pts - curr . dts ) , - 18000 ) , 0 ) ;
305+ if ( PTSDTSshift < 0 ) {
306+ logger . warn ( `[mp4-remuxer]: PTS < DTS detected in video samples, shifting DTS by ${ Math . round ( PTSDTSshift / 90 ) } ms to overcome this issue` ) ;
307+ for ( let i = 0 ; i < inputSamples . length ; i ++ ) {
308+ inputSamples [ i ] . dts = Math . max ( 0 , inputSamples [ i ] . dts + PTSDTSshift ) ;
309+ }
310+ }
311+
312+ // Clamp first DTS to 0 so that we're still aligning on initPTS,
313+ // and not passing negative values to MP4.traf. This will change initial frame compositionTimeOffset!
314+ firstDTS = Math . max ( inputSamples [ 0 ] . dts , 0 ) ;
315+
310316 let nbNalu = 0 ;
311317 let naluLen = 0 ;
312318 for ( let i = 0 ; i < nbSamples ; i ++ ) {
@@ -386,7 +392,7 @@ export default class MP4Remuxer implements Remuxer {
386392 // If so, playback would potentially get stuck, so we artificially inflate
387393 // the duration of the last frame to minimize any potential gap between segments.
388394 const gapTolerance = Math . floor ( config . maxBufferHole * timeScale ) ;
389- const deltaToFrameEnd = ( audioTrackLength ? firstPTS + audioTrackLength * timeScale : this . nextAudioPts ) - avcSample . pts ;
395+ const deltaToFrameEnd = ( audioTrackLength ? minPTS + audioTrackLength * timeScale : this . nextAudioPts ) - avcSample . pts ;
390396 if ( deltaToFrameEnd > gapTolerance ) {
391397 // We subtract lastFrameDuration from deltaToFrameEnd to try to prevent any video
392398 // frame overlap. maxBufferHole should be >> lastFrameDuration anyway.
@@ -429,8 +435,8 @@ export default class MP4Remuxer implements Remuxer {
429435 const data = {
430436 data1 : moof ,
431437 data2 : mdat ,
432- startPTS : firstPTS / timeScale ,
433- endPTS : ( lastPTS + mp4SampleDuration ) / timeScale ,
438+ startPTS : minPTS / timeScale ,
439+ endPTS : ( maxPTS + mp4SampleDuration ) / timeScale ,
434440 startDTS : firstDTS / timeScale ,
435441 endDTS : nextAvcDts / timeScale ,
436442 type,
@@ -518,9 +524,17 @@ export default class MP4Remuxer implements Remuxer {
518524
519525 // If we're overlapping by more than a duration, drop this sample
520526 if ( delta <= - maxAudioFramesDrift * inputSampleDuration ) {
521- logger . warn ( `[mp4-remuxer]: Dropping 1 audio frame @ ${ ( nextPts / inputTimeScale ) . toFixed ( 3 ) } s due to ${ Math . round ( duration ) } ms overlap.` ) ;
522- inputSamples . splice ( i , 1 ) ;
523- // Don't touch nextPtsNorm or i
527+ if ( contiguous ) {
528+ logger . warn ( `[mp4-remuxer]: Dropping 1 audio frame @ ${ ( nextPts / inputTimeScale ) . toFixed ( 3 ) } s due to ${ Math . round ( duration ) } ms overlap.` ) ;
529+ inputSamples . splice ( i , 1 ) ;
530+ // Don't touch nextPtsNorm or i
531+ } else {
532+ // When changing qualities we can't trust that audio has been appended up to nextAudioPts
533+ // Warn about the overlap but do not drop samples as that can introduce buffer gaps
534+ logger . warn ( `Audio frame @ ${ ( pts / inputTimeScale ) . toFixed ( 3 ) } s overlaps nextAudioPts by ${ Math . round ( 1000 * delta / inputTimeScale ) } ms.` ) ;
535+ nextPts = pts + inputSampleDuration ;
536+ i ++ ;
537+ }
524538 } // eslint-disable-line brace-style
525539
526540 // Insert missing frames if:
0 commit comments